Repository: apache/streampipes
Pull request #2709 (2024-04-07 10:32:37)

refactor(#2708): Make all methods of FileManager none static
<!--
  ~ 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.
  ~
  -->
  
  <!--
Thanks for contributing! Here are some tips you can follow to help us incorporate your contribution quickly and easily:
1. If this is your first time, please read our contributor guidelines:
    - https://streampipes.apache.org/getinvolved.html
    - https://cwiki.apache.org/confluence/display/STREAMPIPES/Getting+Started
2. Make sure the PR title is formatted like: `[#<GitHub issue id>] PR title ...`
3. If the PR is unfinished, add '[WIP]' in your PR title, e.g., `[WIP][#<GitHub issue id>] PR title ...`.
4. Please write your PR title to summarize what this PR proposes/fixes.
5. Link the PR to the corresponding GitHub issue (if present) in the `Development` section in the right menu bar. 
6. Be sure to keep the PR description updated to reflect all changes.
7. If possible, provide a concise example to reproduce the issue for a faster review.
8. Make sure tests pass via `mvn clean install`.
9. (Optional) If the contribution is large, please file an Apache ICLA
    - http://apache.org/licenses/icla.pdf
-->

### Purpose
<!--
Please clarify what changes you are proposing and describe how those changes will address the issue.
Furthermore, describe potential consequences the changes might have.
-->

### Remarks
<!--
Is there anything left we need to pay attention on?
Are there some references that might be important? E.g. links to Confluence, or discussions
on the mailing list or GitHub.
-->
PR introduces (a) breaking change(s): no

PR introduces (a) deprecation(s): no



Patch:
diff --git a/pom.xml b/pom.xml
index 514a7f8e9f..23b6ca422c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -91,7 +91,7 @@
         <jts-core.version>1.19.0</jts-core.version>
         <kafka.version>3.4.0</kafka.version>
         <lightcouch.version>0.2.0</lightcouch.version>
-        <maven-plugin-annotations.version>3.11.0</maven-plugin-annotations.version>
+        <maven-plugin-annotations.version>3.12.0</maven-plugin-annotations.version>
         <mailapi.version>1.4.3</mailapi.version>
         <micrometer-prometheus.version>1.12.0</micrometer-prometheus.version>
         <micrometer-observation.version>1.12.0</micrometer-observation.version>
diff --git a/streampipes-client-python/poetry.lock b/streampipes-client-python/poetry.lock
index 2788568499..e090be3d2a 100644
--- a/streampipes-client-python/poetry.lock
+++ b/streampipes-client-python/poetry.lock
@@ -951,13 +951,13 @@ files = [
 
 [[package]]
 name = "interrogate"
-version = "1.5.0"
+version = "1.7.0"
 description = "Interrogate a codebase for docstring coverage."
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.8"
 files = [
-    {file = "interrogate-1.5.0-py3-none-any.whl", hash = "sha256:a4ccc5cbd727c74acc98dee6f5e79ef264c0bcfa66b68d4e123069b2af89091a"},
-    {file = "interrogate-1.5.0.tar.gz", hash = "sha256:b6f325f0aa84ac3ac6779d8708264d366102226c5af7d69058cecffcff7a6d6c"},
+    {file = "interrogate-1.7.0-py3-none-any.whl", hash = "sha256:b13ff4dd8403369670e2efe684066de9fcb868ad9d7f2b4095d8112142dc9d12"},
+    {file = "interrogate-1.7.0.tar.gz", hash = "sha256:a320d6ec644dfd887cc58247a345054fc4d9f981100c45184470068f4b3719b0"},
 ]
 
 [package.dependencies]
@@ -967,13 +967,13 @@ click = ">=7.1"
 colorama = "*"
 py = "*"
 tabulate = "*"
-toml = "*"
+tomli = {version = "*", markers = "python_version < \"3.11\""}
 
 [package.extras]
-dev = ["cairosvg", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "sphinx", "sphinx-autobuild", "wheel"]
+dev = ["cairosvg", "coverage[toml]", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "sphinx", "sphinx-autobuild", "wheel"]
 docs = ["sphinx", "sphinx-autobuild"]
 png = ["cairosvg"]
-tests = ["pytest", "pytest-cov", "pytest-mock"]
+tests = ["coverage[toml]", "pytest", "pytest-cov", "pytest-mock"]
 
 [[package]]
 name = "ipython"
@@ -3632,13 +3632,13 @@ files = [
 
 [[package]]
 name = "typing-extensions"
-version = "4.10.0"
+version = "4.11.0"
 description = "Backported and Experimental Type Hints for Python 3.8+"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"},
-    {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"},
+    {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"},
+    {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"},
 ]
 
 [[package]]
@@ -3826,4 +3826,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
 [metadata]
 lock-version = "2.0"
 python-versions = ">3.8.1,<3.12"
-content-hash = "defd7f0109cd32ae300c3255d7132f9694a145f8e9f765c19a3962c93e12856e"
+content-hash = "fa7153515f8a8be13eb4d2b58eeb48add0290789bf49a864f2df57bfc0bd258d"
diff --git a/streampipes-client-python/pyproject.toml b/streampipes-client-python/pyproject.toml
index 4eff61e87f..e80581b03c 100644
--- a/streampipes-client-python/pyproject.toml
+++ b/streampipes-client-python/pyproject.toml
@@ -69,7 +69,7 @@ autoflake = "2.3.0"
 black = "24.3.0"
 blacken-docs = "1.16.0"
 flake8 = "6.1.0"
-interrogate = { version = "1.5.0", extras = ["png"] }
+interrogate = { version = "1.7.0", extras = ["png"] }
 isort = "5.13.0"
 mypy = "1.9.0"
 ruff = "0.3.0"
diff --git a/streampipes-commons/src/main/java/org/apache/streampipes/commons/file/FileHasher.java b/streampipes-commons/src/main/java/org/apache/streampipes/commons/file/FileHasher.java
index 8aac95a126..7b8c30eb62 100644
--- a/streampipes-commons/src/main/java/org/apache/streampipes/commons/file/FileHasher.java
+++ b/streampipes-commons/src/main/java/org/apache/streampipes/commons/file/FileHasher.java
@@ -28,7 +28,12 @@ import java.nio.file.Paths;
 
 public class FileHasher {
 
-  public static String hash(File file) throws IOException {
+  public String hash(File file) throws IOException {
+
+    if (file == null) {
+      throw new IOException("Input file is null. Please provide a valid file to calculate the hash.");
+    }
+
     try (InputStream is = Files.newInputStream(Paths.get(file.toURI()))) {
       return DigestUtils.md5Hex(is);
     }
diff --git a/streampipes-data-export/src/main/java/org/apache/streampipes/export/generator/ExportPackageGenerator.java b/streampipes-data-export/src/main/java/org/apache/streampipes/export/generator/ExportPackageGenerator.java
index fc3663929f..13abd92077 100644
--- a/streampipes-data-export/src/main/java/org/apache/streampipes/export/generator/ExportPackageGenerator.java
+++ b/streampipes-data-export/src/main/java/org/apache/streampipes/export/generator/ExportPackageGenerator.java
@@ -124,7 +124,7 @@ public class ExportPackageGenerator {
         String filename = fileResolver.findDocument(item.getResourceId()).getFilename();
         addDoc(builder, item, new FileResolver(), manifest::addFile);
         try {
-          builder.addBinary(filename, Files.readAllBytes(FileManager.getFile(filename).toPath()));
+          builder.addBinary(filename, Files.readAllBytes(new FileManager().getFile(filename).toPath()));
         } catch (IOException e) {
           e.printStackTrace();
         }
diff --git a/streampipes-extensions/streampipes-connect-adapters-iiot/src/main/java/org/apache/streampipes/connect/iiot/utils/FileProtocolUtils.java b/streampipes-extensions/streampipes-connect-adapters-iiot/src/main/java/org/apache/streampipes/connect/iiot/utils/FileProtocolUtils.java
index 91f0867a11..6c7ece4c44 100644
--- a/streampipes-extensions/streampipes-connect-adapters-iiot/src/main/java/org/apache/streampipes/connect/iiot/utils/FileProtocolUtils.java
+++ b/streampipes-extensions/streampipes-connect-adapters-iiot/src/main/java/org/apache/streampipes/connect/iiot/utils/FileProtocolUtils.java
@@ -56,7 +56,7 @@ public class FileProtocolUtils {
    */
   private static boolean checkIfFileChanged(String selectedFilename) {
     try {
-      var hash = FileHasher.hash(getFile(selectedFilename));
+      var hash = new FileHasher().hash(getFile(selectedFilename));
       StreamPipesClient client = getStreamPipesClientInstance();
       return client.fileApi()
                    .checkFileContentChanged(selectedFilename, hash);
diff --git a/streampipes-model/src/main/java/org/apache/streampipes/model/file/FileMetadata.java b/streampipes-model/src/main/java/org/apache/streampipes/model/file/FileMetadata.java
index f4ee123ecc..4696179903 100644
--- a/streampipes-model/src/main/java/org/apache/streampipes/model/file/FileMetadata.java
+++ b/streampipes-model/src/main/java/org/apache/streampipes/model/file/FileMetadata.java
@@ -91,4 +91,5 @@ public class FileMetadata {
   public void setFiletype(String filetype) {
     this.filetype = filetype;
   }
+
 }
diff --git a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/file/FileManager.java b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/file/FileManager.java
index 5038ec3af0..84bb7f47b7 100644
--- a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/file/FileManager.java
+++ b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/file/FileManager.java
@@ -34,17 +34,38 @@ import java.util.stream.Collectors;
 
 public class FileManager {
 
-  public static List<FileMetadata> getAllFiles() {
+  private final IFileMetadataStorage fileMetadataStorage;
+  private final FileHandler fileHandler;
+  private final FileHasher fileHasher;
+
+  public FileManager(IFileMetadataStorage fileMetadataStorage,
+                     FileHandler fileHandler,
+                     FileHasher fileHasher) {
+    this.fileMetadataStorage = fileMetadataStorage;
+    this.fileHandler = fileHandler;
+    this.fileHasher = fileHasher;
+  }
+
+  public FileManager() {
+    this.fileMetadataStorage = StorageDispatcher
+        .INSTANCE
+        .getNoSqlStore()
+        .getFileMetadataStorage();
+    this.fileHandler = new FileHandler();
+    this.fileHasher = new FileHasher();
+  }
+
+  public List<FileMetadata> getAllFiles() {
     return getAllFiles(null);
   }
 
-  public static List<FileMetadata> getAllFiles(String filetypes) {
-    List<FileMetadata> allFiles = getFileMetadataStorage().getAllFileMetadataDescriptions();
+  public List<FileMetadata> getAllFiles(String filetypes) {
+    List<FileMetadata> allFiles = fileMetadataStorage.getAllFileMetadataDescriptions();
     return filetypes != null ? filterFiletypes(allFiles, filetypes) : allFiles;
   }
 
-  public static File getFile(String filename) {
-    return new FileHandler().getFile(filename);
+  public File getFile(String filename) {
+    return fileHandler.getFile(filename);
   }
 
   /**
@@ -56,7 +77,7 @@ public class FileManager {
    * @param fileInputStream content of file
    * @return metadata of file
    */
-  public static FileMetadata storeFile(String user,
+  public FileMetadata storeFile(String user,
                                        String filename,
                                        InputStream fileInputStream) throws IOException {
 
@@ -72,13 +93,15 @@ public class FileManager {
   }
 
 
-  public static void deleteFile(String id) {
-    FileMetadata fileMetadata = getFileMetadataStorage().getMetadataById(id);
-    new FileHandler().deleteFile(fileMetadata.getFilename());
-    getFileMetadataStorage().deleteFileMetadata(id);
+  public void deleteFile(String id) {
+    var fileMetadata = fileMetadataStorage.getMetadataById(id);
+    if (fileMetadata != null) {
+      fileHandler.deleteFile(fileMetadata.getFilename());
+      fileMetadataStorage.deleteFileMetadata(id);
+    }
   }
 
-  private static InputStream validateFileNameAndCleanFile(String filename,
+  private InputStream validateFileNameAndCleanFile(String filename,
                                                   String filetype,
                                                   InputStream fileInputStream) {
     if (!validateFileType(filename)) {
@@ -95,7 +118,7 @@ public class FileManager {
    * @param filetype file of type
    * @return input stream without BOM
    */
-  protected static InputStream cleanFile(InputStream fileInputStream, String filetype) {
+  protected InputStream cleanFile(InputStream fileInputStream, String filetype) {
     if (Filetypes.CSV.getFileExtensions().contains(filetype.toLowerCase())) {
       fileInputStream = new BOMInputStream(fileInputStream);
     }
@@ -103,34 +126,26 @@ public class FileManager {
     return fileInputStream;
   }
 
-  public static boolean checkFileContentChanged(String filename, String hash) throws IOException {
-    var fileHash = FileHasher.hash(getFile(filename));
+  public boolean checkFileContentChanged(String filename, String hash) throws IOException {
+    var fileHash = fileHasher.hash(getFile(filename));
     return !fileHash.equals(hash);
   }
 
-  public static String sanitizeFilename(String filename) {
+  public String sanitizeFilename(String filename) {
     return filename.replaceAll("[^a-zA-Z0-9.\\-]", "_");
   }
 
-  public static boolean validateFileType(String filename) {
+  public boolean validateFileType(String filename) {
     return Filetypes.getAllFileExtensions()
                     .stream()
                     .anyMatch(filename::endsWith);
   }
 
-  protected static void writeToFile(String sanitizedFilename, InputStream fileInputStream) throws IOException {
-    new FileHandler().storeFile(sanitizedFilename, fileInputStream);
-  }
-
-
-  private static IFileMetadataStorage getFileMetadataStorage() {
-    return StorageDispatcher
-        .INSTANCE
-        .getNoSqlStore()
-        .getFileMetadataStorage();
+  protected void writeToFile(String sanitizedFilename, InputStream fileInputStream) throws IOException {
+    fileHandler.storeFile(sanitizedFilename, fileInputStream);
   }
 
-  protected static FileMetadata makeAndStoreFileMetadata(String user,
+  protected FileMetadata makeAndStoreFileMetadata(String user,
                                                        String sanitizedFilename,
                                                        String filetype) {
     var fileMetadata = makeFileMetadata(user, sanitizedFilename, filetype);
@@ -139,7 +154,7 @@ public class FileManager {
     return fileMetadata;
   }
 
-  private static FileMetadata makeFileMetadata(String user,
+  private FileMetadata makeFileMetadata(String user,
                                                String filename,
                                                String filetype) {
 
@@ -152,12 +167,12 @@ public class FileManager {
     return fileMetadata;
   }
 
-  private static void storeFileMetadata(FileMetadata fileMetadata) {
-    getFileMetadataStorage().addFileMetadata(fileMetadata);
+  private void storeFileMetadata(FileMetadata fileMetadata) {
+    fileMetadataStorage.addFileMetadata(fileMetadata);
   }
 
 
-  private static List<FileMetadata> filterFiletypes(List<FileMetadata> allFiles, String filetypes) {
+  private List<FileMetadata> filterFiletypes(List<FileMetadata> allFiles, String filetypes) {
     return allFiles
         .stream()
         .filter(fileMetadata -> Arrays
diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/ResetManagement.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/ResetManagement.java
index 8319d0a4ee..d5e89b9671 100644
--- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/ResetManagement.java
+++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/ResetManagement.java
@@ -131,8 +131,9 @@ public class ResetManagement {
   }
 
   private static void deleteAllFiles() {
-    List<FileMetadata> allFiles = FileManager.getAllFiles();
-    allFiles.forEach(fileMetadata -> FileManager.deleteFile(fileMetadata.getFileId()));
+    var fileManager = new FileManager();
+    List<FileMetadata> allFiles = fileManager.getAllFiles();
+    allFiles.forEach(fileMetadata -> fileManager.deleteFile(fileMetadata.getFileId()));
   }
 
   private static void removeAllDataInDataLake() {
diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/AssetDashboardResource.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/AssetDashboardResource.java
index 4433d51ae4..4687f7c962 100644
--- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/AssetDashboardResource.java
+++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/AssetDashboardResource.java
@@ -106,7 +106,7 @@ public class AssetDashboardResource extends AbstractRestResource {
   @GetMapping(path = "/images/{imageName}")
   public ResponseEntity<byte[]> getDashboardImage(@PathVariable("imageName") String imageName) {
     try {
-      var sanitizedFileName = FileManager.sanitizeFilename(imageName);
+      var sanitizedFileName = new FileManager().sanitizeFilename(imageName);
       java.nio.file.Path path = Paths.get(getTargetFile(sanitizedFileName));
       File file = new File(path.toString());
       FileNameMap fileNameMap = URLConnection.getFileNameMap();
@@ -128,7 +128,7 @@ public class AssetDashboardResource extends AbstractRestResource {
     try {
       var fileName = fileDetail.getName();
 
-      if (!FileManager.validateFileType(fileName)) {
+      if (!new FileManager().validateFileType(fileName)) {
         LOG.error("File type of file " + fileName + " is not supported");
         fail();
       }
@@ -148,7 +148,7 @@ public class AssetDashboardResource extends AbstractRestResource {
       targetDirectory.mkdirs();
     }
 
-    var sanitizedFileName = FileManager.sanitizeFilename(fileName);
+    var sanitizedFileName = new FileManager().sanitizeFilename(fileName);
 
     var targetFile = new File(getTargetFile(sanitizedFileName));
 
diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineElementFile.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineElementFile.java
index 89373cce05..006f3f955d 100644
--- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineElementFile.java
+++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineElementFile.java
@@ -53,6 +53,12 @@ import static org.springframework.http.HttpStatus.NOT_FOUND;
 @RequestMapping("/api/v2/files")
 public class PipelineElementFile extends AbstractAuthGuardedRestResource {
 
+  private final FileManager fileManager;
+
+  public PipelineElementFile() {
+    this.fileManager = new FileManager();
+  }
+
   @PostMapping(
       consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
       produces = MediaType.APPLICATION_JSON_VALUE)
@@ -60,7 +66,7 @@ public class PipelineElementFile extends AbstractAuthGuardedRestResource {
   public ResponseEntity<?> storeFile(@RequestPart("file_upload") MultipartFile fileDetail) {
     try {
       FileMetadata metadata =
-          FileManager.storeFile(
+          fileManager.storeFile(
               getAuthenticatedUsername(),
               fileDetail.getOriginalFilename(),
               fileDetail.getInputStream()
@@ -74,7 +80,7 @@ public class PipelineElementFile extends AbstractAuthGuardedRestResource {
   @DeleteMapping(path = "{fileId}")
   @PreAuthorize(AuthConstants.IS_ADMIN_ROLE)
   public ResponseEntity<Void> deleteFile(@PathVariable("fileId") String fileId) {
-    FileManager.deleteFile(fileId);
+    fileManager.deleteFile(fileId);
     return ok();
   }
 
@@ -83,7 +89,7 @@ public class PipelineElementFile extends AbstractAuthGuardedRestResource {
   public ResponseEntity<List<FileMetadata>> getFileInfo(
       @RequestParam(value = "filetypes", required = false) String filetypes
   ) {
-    return ok(FileManager.getAllFiles(filetypes));
+    return ok(fileManager.getAllFiles(filetypes));
   }
 
   @GetMapping(path = "/{filename}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
@@ -109,7 +115,7 @@ public class PipelineElementFile extends AbstractAuthGuardedRestResource {
       @PathVariable("filename") String filename
   ) {
     try {
-      return ok(getFileContents(FileManager.getFile(filename)));
+      return ok(getFileContents(fileManager.getFile(filename)));
     } catch (IOException e) {
       throw new SpMessageException(
           NOT_FOUND,
@@ -125,7 +131,7 @@ public class PipelineElementFile extends AbstractAuthGuardedRestResource {
   @GetMapping(path = "/allFilenames", produces = MediaType.APPLICATION_JSON_VALUE)
   @PreAuthorize(AuthConstants.HAS_READ_FILE_PRIVILEGE)
   public ResponseEntity<List<String>> getAllOriginalFilenames() {
-    return ok(FileManager.getAllFiles()
+    return ok(fileManager.getAllFiles()
                          .stream()
                          .map(fileMetadata -> fileMetadata.getFilename()
                                                           .toLowerCase())
@@ -141,7 +147,7 @@ public class PipelineElementFile extends AbstractAuthGuardedRestResource {
       @PathVariable(value = "hash") String hash
   ) {
     try {
-      return ok(FileManager.checkFileContentChanged(filename, hash));
+      return ok(fileManager.checkFileContentChanged(filename, hash));
     } catch (IOException e) {
       throw new SpMessageException(
           NOT_FOUND,
diff --git a/ui/package-lock.json b/ui/package-lock.json
index 0230985823..d339898434 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -10,16 +10,16 @@
       "dependencies": {
         "@ali-hm/angular-tree-component": "12.0.5",
         "@angular-architects/native-federation": "^17.1.7",
-        "@angular/animations": "^17.3.2",
-        "@angular/cdk": "^17.3.2",
-        "@angular/common": "^17.3.2",
-        "@angular/compiler": "^17.3.2",
-        "@angular/core": "^17.3.2",
-        "@angular/forms": "^17.3.2",
-        "@angular/material": "^17.3.2",
-        "@angular/platform-browser": "^17.3.2",
-        "@angular/platform-browser-dynamic": "^17.3.2",
-        "@angular/router": "^17.3.2",
+        "@angular/animations": "^17.3.3",
+        "@angular/cdk": "^17.3.3",
+        "@angular/common": "^17.3.3",
+        "@angular/compiler": "^17.3.3",
+        "@angular/core": "^17.3.3",
+        "@angular/forms": "^17.3.3",
+        "@angular/material": "^17.3.3",
+        "@angular/platform-browser": "^17.3.3",
+        "@angular/platform-browser-dynamic": "^17.3.3",
+        "@angular/router": "^17.3.3",
         "@asymmetrik/ngx-leaflet": "^17.0.0",
         "@auth0/angular-jwt": "^5.2.0",
         "@ctrl/ngx-codemirror": "7.0.0",
@@ -64,20 +64,20 @@
         "zone.js": "~0.14.4"
       },
       "devDependencies": {
-        "@angular-devkit/build-angular": "^17.3.2",
+        "@angular-devkit/build-angular": "^17.3.3",
         "@angular-eslint/builder": "17.3.0",
         "@angular-eslint/eslint-plugin": "^17.3.0",
         "@angular-eslint/eslint-plugin-template": "^17.3.0",
         "@angular-eslint/schematics": "^17.3.0",
         "@angular-eslint/template-parser": "^17.3.0",
-        "@angular/cli": "^17.3.2",
-        "@angular/compiler-cli": "^17.3.2",
+        "@angular/cli": "^17.3.3",
+        "@angular/compiler-cli": "^17.3.3",
         "@types/angular": "^1.8.4",
         "@types/jasmine": "~5.1.0",
         "@types/jqueryui": "^1.12.16",
         "@types/jshint": "^2.12.1",
         "@types/leaflet": "^1.9.0",
-        "@types/node": "^20.10.4",
+        "@types/node": "^20.12.5",
         "@types/rx": "^4.1.2",
         "@types/showdown": "2.0.6",
         "@typescript-eslint/eslint-plugin": "5.62.0",
@@ -294,12 +294,12 @@
       }
     },
     "node_modules/@angular-devkit/architect": {
-      "version": "0.1703.2",
-      "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.2.tgz",
-      "integrity": "sha512-fT5gSzwDHOyGv8zF97t8rjeoYSGSxXjWWstl3rN1nXdO0qgJ5m6Sv0fupON+HltdXDCBLRH+2khNpqx/Fh0Qww==",
+      "version": "0.1703.3",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.3.tgz",
+      "integrity": "sha512-BKbdigCjmspqxOxSIQuWgPZzpyuKqZoTBDh0jDeLcAmvPsuxCgIWbsExI4OQ0CyusnQ+XT0IT39q8B9rvF56cg==",
       "dev": true,
       "dependencies": {
-        "@angular-devkit/core": "17.3.2",
+        "@angular-devkit/core": "17.3.3",
         "rxjs": "7.8.1"
       },
       "engines": {
@@ -309,15 +309,15 @@
       }
     },
     "node_modules/@angular-devkit/build-angular": {
-      "version": "17.3.2",
-      "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.3.2.tgz",
-      "integrity": "sha512-muPCUyL0uHvRkLH4NLWiccER6P2vCm/Q5DDvqyN4XOzzY3tAHHLrKrpvY87sgd2oNJ6Ci8x7GPNcfzR5KELCnw==",
+      "version": "17.3.3",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.3.3.tgz",
+      "integrity": "sha512-E/6Z1MIMhEB1I2sN+Pw4/zinwAFj4vLDh6dEuj856WWEPndgPiUB6fGX4EbCTsyIUzboXI5ysdNyt2Eq56bllA==",
       "dev": true,
       "dependencies": {
         "@ampproject/remapping": "2.3.0",
-        "@angular-devkit/architect": "0.1703.2",
-        "@angular-devkit/build-webpack": "0.1703.2",
-        "@angular-devkit/core": "17.3.2",
+        "@angular-devkit/architect": "0.1703.3",
+        "@angular-devkit/build-webpack": "0.1703.3",
+        "@angular-devkit/core": "17.3.3",
         "@babel/core": "7.24.0",
         "@babel/generator": "7.23.6",
         "@babel/helper-annotate-as-pure": "7.22.5",
@@ -328,7 +328,7 @@
         "@babel/preset-env": "7.24.0",
         "@babel/runtime": "7.24.0",
         "@discoveryjs/json-ext": "0.5.7",
-        "@ngtools/webpack": "17.3.2",
+        "@ngtools/webpack": "17.3.3",
         "@vitejs/plugin-basic-ssl": "1.1.0",
         "ansi-colors": "4.1.3",
         "autoprefixer": "10.4.18",
@@ -854,12 +854,12 @@
       }
     },
     "node_modules/@angular-devkit/build-webpack": {
-      "version": "0.1703.2",
-      "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.2.tgz",
-      "integrity": "sha512-w7rVFQcZK4iTCd/MLfQWIkDkwBOfAs++txNQyS9qYID8KvLs1V+oWYd2qDBRelRv1u3YtaCIS1pQx3GFKBC3OA==",
+      "version": "0.1703.3",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.3.tgz",
+      "integrity": "sha512-d0JjE8MaGVNphlJfeP1OZKhNT4wCXkEZKdSdwE0+W+vDHNUuZiUBB1czO48sb7T4xBrdjRWlV/9CzMNJ7n3ydA==",
       "dev": true,
       "dependencies": {
-        "@angular-devkit/architect": "0.1703.2",
+        "@angular-devkit/architect": "0.1703.3",
         "rxjs": "7.8.1"
       },
       "engines": {
@@ -873,9 +873,9 @@
       }
     },
     "node_modules/@angular-devkit/core": {
-      "version": "17.3.2",
-      "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.2.tgz",
-      "integrity": "sha512-1vxKo9+pdSwTOwqPDSYQh84gZYmCJo6OgR5+AZoGLGMZSeqvi9RG5RiUcOMLQYOnuYv0arlhlWxz0ZjyR8ApKw==",
+      "version": "17.3.3",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.3.tgz",
+      "integrity": "sha512-J22Sh3M7rj8Ar3iEs20ko5wgC3DE7vWfYZNdimt2IJiS4J7BEX8R3Awf+TRt+6AN3NFm3/xe1Sz4yvDh3FvNFg==",
       "dev": true,
       "dependencies": {
         "ajv": "8.12.0",
@@ -900,12 +900,12 @@
       }
     },
     "node_modules/@angular-devkit/schematics": {
-      "version": "17.3.2",
-      "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.2.tgz",
-      "integrity": "sha512-AYO6oc6QpFGigc1KiDzEVT1CeLnwvnIedU5Q/U3JDZ/Yqmvgc09D64g9XXER2kg6tV7iEgLxiYnonIAQOHq7eA==",
+      "version": "17.3.3",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.3.tgz",
+      "integrity": "sha512-SABqTtj2im4PJhQjNaAsSypbNkpZFW8YozJ3P748tlh5a9XoHpgiqXv5JhRbyKElLDAyk5i9fe2++JmSudPG/Q==",
       "dev": true,
       "dependencies": {
-        "@angular-devkit/core": "17.3.2",
+        "@angular-devkit/core": "17.3.3",
         "jsonc-parser": "3.2.1",
         "magic-string": "0.30.8",
         "ora": "5.4.1",
@@ -1016,9 +1016,9 @@
       }
     },
     "node_modules/@angular/animations": {
-      "version": "17.3.2",
-      "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.3.2.tgz",
-      "integrity": "sha512-9RplCRS3dS7I8UeMmnwVCAxEaixQCj98UkSqjErO+GX5KJwMsFPydh7HKWH0/yclidJe5my41psEiQkyEyGKww==",
+      "version": "17.3.3",
+      "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.3.3.tgz",
+      "integrity": "sha512-poLW3FHe5wkxmTIsQ3em2vq4obgQHyZJz6biF+4hCqQSNMbMBS0e5ZycAiJLkUD/WLc88lQZ20muRO7qjVuMLA==",
       "dependencies": {
         "tslib": "^2.3.0"
       },
@@ -1026,13 +1026,13 @@
         "node": "^18.13.0 || >=20.9.0"
       },
       "peerDependencies": {
-        "@angular/core": "17.3.2"
+        "@angular/core": "17.3.3"
       }
     },
     "node_modules/@angular/cdk": {
-      "version": "17.3.2",
-      "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.3.2.tgz",
-      "integrity": "sha512-mC2U7aoIf7RSpGgIwVyfQEbaPDDX59plQt88KeTz15wjF8vosLt2DG0rZEoV8Mq14YS47J+jI76q/LJfd6/GCw==",
+      "version": "17.3.3",
+      "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.3.3.tgz",
+      "integrity": "sha512-hfS9pwaNE6CTZqP3FBh9tZPbuf//bDqZ5IpMzscfDFrwX8ycxBiI3znH/rFSf9l1rL0OQGoqWWNVfJCT+RrukA==",
       "dependencies": {
         "tslib": "^2.3.0"
       },
@@ -1046,15 +1046,15 @@
       }
     },
     "node_modules/@angular/cli": {
-      "version": "17.3.2",
-      "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.3.2.tgz",
-      "integrity": "sha512-g6r4XZyGnh9P6GmWgaFh8RmR4L6UdQ408e3SpG3rjncuPRD57Ur8806GfCLPt6HIA9TARiKmaJ0EJ3RsIjag0g==",
+      "version": "17.3.3",
+      "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.3.3.tgz",
+      "integrity": "sha512-veIGK2sRm0SfiLHeftx0W0xC3N8uxoqxXiSG57V6W2wIFN/fKm3aRq3sa8phz7vxUzoKGqyZh6hsT7ybkjgkGA==",
       "dev": true,
       "dependencies": {
-        "@angular-devkit/architect": "0.1703.2",
-        "@angular-devkit/core": "17.3.2",
-        "@angular-devkit/schematics": "17.3.2",
-        "@schematics/angular": "17.3.2",
+        "@angular-devkit/architect": "0.1703.3",
+        "@angular-devkit/core": "17.3.3",
+        "@angular-devkit/schematics": "17.3.3",
+        "@schematics/angular": "17.3.3",
         "@yarnpkg/lockfile": "1.1.0",
         "ansi-colors": "4.1.3",
         "ini": "4.1.2",
@@ -1080,9 +1080,9 @@
       }
     },
     "node_modules/@angular/common": {
-      "version": "17.3.2",
-      "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.3.2.tgz",
-      "integrity": "sha512-7fo+hrQEzo+VX0fJAKK+P4YNeiEnpdMOAkyIdwweyAeUZYeFIs6TKtax3CiJAubnkIkhQ/52uxiusDhK3Wg/WQ==",
+      "version": "17.3.3",
+      "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.3.3.tgz",
+      "integrity": "sha512-GwlKetNpfWKiG2j4S6bYTi6PA2iT4+eln7o8owo44xZWdQnWQjfxnH39vQuCyhi6OOQL1dozmae+fVXgQsV6jQ==",
       "dependencies": {
         "tslib": "^2.3.0"
       },
@@ -1090,14 +1090,14 @@
         "node": "^18.13.0 || >=20.9.0"
       },
       "peerDependencies": {
-        "@angular/core": "17.3.2",
+        "@angular/core": "17.3.3",
         "rxjs": "^6.5.3 || ^7.4.0"
       }
     },
     "node_modules/@angular/compiler": {
-      "version": "17.3.2",
-      "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.3.2.tgz",
-      "integrity": "sha512-+/l/FQpVsOPbxZzSKyqEra+yxoI/r8LlTRqshVACv10+DKMWJMHnDkVUrNxvWHutfn4RszpGMtbtHp3yM9rxcA==",
+      "version": "17.3.3",
+      "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.3.3.tgz",
+      "integrity": "sha512-ZNMRfagMxMjk1KW5H3ssCg5QL0J6ZW1JAZ1mrTXixqS7gbdwl60bTGE+EfuEwbjvovEYaj4l9cga47eMaxZTbQ==",
       "dependencies": {
         "tslib": "^2.3.0"
       },
@@ -1105,7 +1105,7 @@
         "node": "^18.13.0 || >=20.9.0"
       },
       "peerDependencies": {
-        "@angular/core": "17.3.2"
+        "@angular/core": "17.3.3"
       },
       "peerDependenciesMeta": {
         "@angular/core": {
@@ -1114,9 +1114,9 @@
       }
     },
     "node_modules/@angular/compiler-cli": {
-      "version": "17.3.2",
-      "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.3.2.tgz",
-      "integrity": "sha512-PG81BrJjeF679tkafjt+t9VEBE1rPq39cdLoBTnPY7Q+E/thVoem5JTRG6hmnLmwEc0xxY6sfYpvx2BB5ywUSA==",
+      "version": "17.3.3",
+      "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.3.3.tgz",
+      "integrity": "sha512-vM0lqwuXQZ912HbLnIuvUblvIz2WEUsU7a5Z2ieNey6famH4zxPH12vCbVwXgicB6GLHorhOfcWC5443wD2mJw==",
       "dev": true,
       "dependencies": {
         "@babel/core": "7.23.9",
@@ -1137,7 +1137,7 @@
         "node": "^18.13.0 || >=20.9.0"
       },
       "peerDependencies": {
-        "@angular/compiler": "17.3.2",
+        "@angular/compiler": "17.3.3",
         "typescript": ">=5.2 <5.5"
       }
     },
@@ -1187,9 +1187,9 @@
       }
     },
     "node_modules/@angular/core": {
-      "version": "17.3.2",
-      "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.3.2.tgz",
-      "integrity": "sha512-eylatBGaN8uihKomEcXkaSHmAea5bEqu1OXifEoVOJiJpJA9Dbt/VcLXkIRFnRGH2NWUT5W79vSoU9GRvPMk5w==",
+      "version": "17.3.3",
+      "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.3.3.tgz",
+      "integrity": "sha512-O/jr3aFJMCxF6Jmymjx4jIigRHJfqM/ALIi60y2LVznBVFkk9xyMTsAjgWQIEHX+2muEIzgfKuXzpL0y30y+wA==",
       "dependencies": {
         "tslib": "^2.3.0"
       },
@@ -1202,9 +1202,9 @@
       }
     },
     "node_modules/@angular/forms": {
-      "version": "17.3.2",
-      "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.3.2.tgz",
-      "integrity": "sha512-sbHYjAEeEWW+02YDEKuuuTEUukm6AayQuHiAu37vACj/2q/2RWQar49IoRcSJfAwP2ckqRSK4mmLoDX4IG/KSg==",
+      "version": "17.3.3",
+      "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.3.3.tgz",
+      "integrity": "sha512-wqn+eAggbOZY91hr7oDjv5qdflszVOC9SZMcWJUoZTGn+8eoV6v6728GDFuDDwYkKQ9G9eQbX4IZmYoVw3TVjQ==",
       "dependencies": {
         "tslib": "^2.3.0"
       },
@@ -1212,16 +1212,16 @@
         "node": "^18.13.0 || >=20.9.0"
       },
       "peerDependencies": {
-        "@angular/common": "17.3.2",
-        "@angular/core": "17.3.2",
-        "@angular/platform-browser": "17.3.2",
+        "@angular/common": "17.3.3",
+        "@angular/core": "17.3.3",
+        "@angular/platform-browser": "17.3.3",
         "rxjs": "^6.5.3 || ^7.4.0"
       }
     },
     "node_modules/@angular/material": {
-      "version": "17.3.2",
-      "resolved": "https://registry.npmjs.org/@angular/material/-/material-17.3.2.tgz",
-      "integrity": "sha512-FuQnzmVZQUqPYxn2AbRNKY/7LTldhazK+PCN+Y9vbTZ6L1INJLANip1r4iXfZnhAUE+dmFAPN6GzfLLh1Dg7vA==",
+      "version": "17.3.3",
+      "resolved": "https://registry.npmjs.org/@angular/material/-/material-17.3.3.tgz",
+      "integrity": "sha512-cb3PYY+Lf3FvXxXIRmOBcTn5QS9Ghr5Eq0aiJiiYV6YVohr0YGWsndMCZ/5a2j8fxpboDo9THeTnOuuAOJv7AA==",
       "dependencies": {
         "@material/animation": "15.0.0-canary.7f224ddd4.0",
         "@material/auto-init": "15.0.0-canary.7f224ddd4.0",
@@ -1274,7 +1274,7 @@
       },
       "peerDependencies": {
         "@angular/animations": "^17.0.0 || ^18.0.0",
-        "@angular/cdk": "17.3.2",
+        "@angular/cdk": "17.3.3",
         "@angular/common": "^17.0.0 || ^18.0.0",
         "@angular/core": "^17.0.0 || ^18.0.0",
         "@angular/forms": "^17.0.0 || ^18.0.0",
@@ -1283,9 +1283,9 @@
       }
     },
     "node_modules/@angular/platform-browser": {
-      "version": "17.3.2",
-      "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.3.2.tgz",
-      "integrity": "sha512-rBVmpJ/uh+CTjYef3Nib1K+31GFbM4mZaw2R2PowKZLgWOT3MWXKy41i44NEyM8qY1dxESmzMzy4NuGfZol42Q==",
+      "version": "17.3.3",
+      "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.3.3.tgz",
+      "integrity": "sha512-XFWjquD+Pr9VszRzrDlT6uaf57TsY9XhL9iHCNok6Op5DpVQpIAuw1vFt2t5ZoQ0gv+lY8mVWnxgqe3CgTdYxw==",
       "dependencies": {
         "tslib": "^2.3.0"
       },
@@ -1293,9 +1293,9 @@
         "node": "^18.13.0 || >=20.9.0"
       },
       "peerDependencies": {
-        "@angular/animations": "17.3.2",
-        "@angular/common": "17.3.2",
-        "@angular/core": "17.3.2"
+        "@angular/animations": "17.3.3",
+        "@angular/common": "17.3.3",
+        "@angular/core": "17.3.3"
       },
       "peerDependenciesMeta": {
         "@angular/animations": {
@@ -1304,9 +1304,9 @@
       }
     },
     "node_modules/@angular/platform-browser-dynamic": {
-      "version": "17.3.2",
-      "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.3.2.tgz",
-      "integrity": "sha512-fcGo9yQ+t9VaG9zPgjQW5HIizbYOKj+9kVk9FPru+uJbYyvJUwEDgpD3aI0DUrQy/OvSf4NMzY/Ucgw1AUknQw==",
+      "version": "17.3.3",
+      "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.3.3.tgz",
+      "integrity": "sha512-jSgSNHRTXCIat20I+4tLm/e8qOvrIE3Zv7S/DtYZEiAth84uoznvo1kXnN+KREse2vP/WoNgSDKQ2JLzkwYXSQ==",
       "dependencies": {
         "tslib": "^2.3.0"
       },
@@ -1314,16 +1314,16 @@
         "node": "^18.13.0 || >=20.9.0"
       },
       "peerDependencies": {
-        "@angular/common": "17.3.2",
-        "@angular/compiler": "17.3.2",
-        "@angular/core": "17.3.2",
-        "@angular/platform-browser": "17.3.2"
+        "@angular/common": "17.3.3",
+        "@angular/compiler": "17.3.3",
+        "@angular/core": "17.3.3",
+        "@angular/platform-browser": "17.3.3"
       }
     },
     "node_modules/@angular/router": {
-      "version": "17.3.2",
-      "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.3.2.tgz",
-      "integrity": "sha512-BJiaG7zldhe8FPsg3Xv1o2xsmWNMIuntubRiSt2NlSceAr/NEgHoARpZfAGKTaFSngl6jc407wHOmBBPPALECw==",
+      "version": "17.3.3",
+      "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.3.3.tgz",
+      "integrity": "sha512-kj42+TtwvET7MFqxB3pkKyob0VNmspASlv8Y29vSpzzaOHn8J1fDf6H+8opoIC+Gmvo5NqXUDwq7nxI5aQ0mUQ==",
       "dependencies": {
         "tslib": "^2.3.0"
       },
@@ -1331,9 +1331,9 @@
         "node": "^18.13.0 || >=20.9.0"
       },
       "peerDependencies": {
-        "@angular/common": "17.3.2",
-        "@angular/core": "17.3.2",
-        "@angular/platform-browser": "17.3.2",
+        "@angular/common": "17.3.3",
+        "@angular/core": "17.3.3",
+        "@angular/platform-browser": "17.3.3",
         "rxjs": "^6.5.3 || ^7.4.0"
       }
     },
@@ -4674,9 +4674,9 @@
       }
     },
     "node_modules/@ngtools/webpack": {
-      "version": "17.3.2",
-      "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.3.2.tgz",
-      "integrity": "sha512-E8zejFF4aJ8l2XcF+GgnE/1IqsZepnPT1xzulLB4LXtjVuXLFLoF9xkHQwxs7cJWWZsxd/SlNsCIcn/ezrYBcQ==",
+      "version": "17.3.3",
+      "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.3.3.tgz",
+      "integrity": "sha512-053KMbg1Tb+Mmg4Htsv8yTpI7ABghguoxhwosQXKB0CjO6M0oexuvdaxbRDQ1vd5xYNOW9LcOfxOMPIwyU4BBA==",
       "dev": true,
       "engines": {
         "node": "^18.13.0 || >=20.9.0",
@@ -5517,13 +5517,13 @@
       }
     },
     "node_modules/@schematics/angular": {
-      "version": "17.3.2",
-      "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.3.2.tgz",
-      "integrity": "sha512-zPINvow0Qo6ionnDl25ZzSSLDyDxBjqRPEJWGHU70expbjXK4A2caQT9P/8ImhapbJAXJCfxg4GF9z1d/sWe4w==",
+      "version": "17.3.3",
+      "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.3.3.tgz",
+      "integrity": "sha512-kNlyjIKTBhfi8Jab3MCkxNRbbpErbzdu0lZNSL8Nidmqs6Tk23Dc1bZe4t/gPNOCkCvQlwYa6X88SjC/ntyVng==",
       "dev": true,
       "dependencies": {
-        "@angular-devkit/core": "17.3.2",
-        "@angular-devkit/schematics": "17.3.2",
+        "@angular-devkit/core": "17.3.3",
+        "@angular-devkit/schematics": "17.3.3",
         "jsonc-parser": "3.2.1"
       },
       "engines": {
@@ -5960,9 +5960,9 @@
       "optional": true
     },
     "node_modules/@types/node": {
-      "version": "20.11.26",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.26.tgz",
-      "integrity": "sha512-YwOMmyhNnAWijOBQweOJnQPl068Oqd4K3OFbTc6AHJwzweUwwWG3GIFY74OKks2PJUDkQPeddOQES9mLn1CTEQ==",
+      "version": "20.12.5",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.5.tgz",
+      "integrity": "sha512-BD+BjQ9LS/D8ST9p5uqBxghlN+S42iuNxjsUGjeZobe/ciXzk2qb1B6IXc6AnRLS+yFJRpN2IPEHMzwspfDJNw==",
       "dependencies": {
         "undici-types": "~5.26.4"
       }
diff --git a/ui/package.json b/ui/package.json
index 0288b6d1b5..7a6b159200 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -31,16 +31,16 @@
   "dependencies": {
     "@ali-hm/angular-tree-component": "12.0.5",
     "@angular-architects/native-federation": "^17.1.7",
-    "@angular/animations": "^17.3.2",
-    "@angular/cdk": "^17.3.2",
-    "@angular/common": "^17.3.2",
-    "@angular/compiler": "^17.3.2",
-    "@angular/core": "^17.3.2",
-    "@angular/forms": "^17.3.2",
-    "@angular/material": "^17.3.2",
-    "@angular/platform-browser": "^17.3.2",
-    "@angular/platform-browser-dynamic": "^17.3.2",
-    "@angular/router": "^17.3.2",
+    "@angular/animations": "^17.3.3",
+    "@angular/cdk": "^17.3.3",
+    "@angular/common": "^17.3.3",
+    "@angular/compiler": "^17.3.3",
+    "@angular/core": "^17.3.3",
+    "@angular/forms": "^17.3.3",
+    "@angular/material": "^17.3.3",
+    "@angular/platform-browser": "^17.3.3",
+    "@angular/platform-browser-dynamic": "^17.3.3",
+    "@angular/router": "^17.3.3",
     "@asymmetrik/ngx-leaflet": "^17.0.0",
     "@auth0/angular-jwt": "^5.2.0",
     "@ctrl/ngx-codemirror": "7.0.0",
@@ -85,20 +85,20 @@
     "zone.js": "~0.14.4"
   },
   "devDependencies": {
-    "@angular-devkit/build-angular": "^17.3.2",
+    "@angular-devkit/build-angular": "^17.3.3",
     "@angular-eslint/builder": "17.3.0",
     "@angular-eslint/eslint-plugin": "^17.3.0",
     "@angular-eslint/eslint-plugin-template": "^17.3.0",
     "@angular-eslint/schematics": "^17.3.0",
     "@angular-eslint/template-parser": "^17.3.0",
-    "@angular/cli": "^17.3.2",
-    "@angular/compiler-cli": "^17.3.2",
+    "@angular/cli": "^17.3.3",
+    "@angular/compiler-cli": "^17.3.3",
     "@types/angular": "^1.8.4",
     "@types/jasmine": "~5.1.0",
     "@types/jqueryui": "^1.12.16",
     "@types/jshint": "^2.12.1",
     "@types/leaflet": "^1.9.0",
-    "@types/node": "^20.10.4",
+    "@types/node": "^20.12.5",
     "@types/rx": "^4.1.2",
     "@types/showdown": "2.0.6",
     "@typescript-eslint/eslint-plugin": "5.62.0",
diff --git a/ui/projects/streampipes/platform-services/package.json b/ui/projects/streampipes/platform-services/package.json
index d4e7118e45..0486014c91 100644
--- a/ui/projects/streampipes/platform-services/package.json
+++ b/ui/projects/streampipes/platform-services/package.json
@@ -2,8 +2,8 @@
   "name": "@streampipes/platform-services",
   "version": "0.0.1",
   "peerDependencies": {
-    "@angular/common": "^17.3.2",
-    "@angular/core": "^17.3.2",
+    "@angular/common": "^17.3.3",
+    "@angular/core": "^17.3.3",
     "rxjs": "^7.5.7",
     "angular-gridster2": "^17.0.0"
   },
diff --git a/ui/projects/streampipes/shared-ui/package.json b/ui/projects/streampipes/shared-ui/package.json
index 542cd6e7f6..f2f8f4d9b5 100644
--- a/ui/projects/streampipes/shared-ui/package.json
+++ b/ui/projects/streampipes/shared-ui/package.json
@@ -2,13 +2,13 @@
   "name": "@streampipes/shared-ui",
   "version": "0.0.1",
   "peerDependencies": {
-    "@angular/animations": "^17.3.2",
-    "@angular/cdk": "^17.3.2",
-    "@angular/common": "^17.3.2",
-    "@angular/core": "^17.3.2",
+    "@angular/animations": "^17.3.3",
+    "@angular/cdk": "^17.3.3",
+    "@angular/common": "^17.3.3",
+    "@angular/core": "^17.3.3",
     "@ngbracket/ngx-layout": "^17.0.1",
-    "@angular/material": "^17.3.2",
-    "@angular/router": "^17.3.2",
+    "@angular/material": "^17.3.3",
+    "@angular/router": "^17.3.3",
     "@streampipes/platform-services": "0.0.1",
     "rxjs": "^7.5.7"
   },


Test patch:
diff --git a/streampipes-commons/src/test/java/org/apache/streampipes/commons/file/FileHasherTest.java b/streampipes-commons/src/test/java/org/apache/streampipes/commons/file/FileHasherTest.java
new file mode 100644
index 0000000000..7b8d1b981d
--- /dev/null
+++ b/streampipes-commons/src/test/java/org/apache/streampipes/commons/file/FileHasherTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.streampipes.commons.file;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class FileHasherTest {
+
+  private FileHasher fileHasher;
+
+  @BeforeEach
+  public void setup() {
+    this.fileHasher = new FileHasher();
+  }
+
+  @Test
+  void hash_returnsCorrectHashForFile() throws IOException {
+    var file = new File("src/test/resources/test.txt");
+    assertEquals("6df4d50a41a5d20bc4faad8a6f09aa8f", fileHasher.hash(file));
+  }
+
+  @Test
+  void hash_throwsIOExceptionForNonExistingFile() {
+    var file = new File("src/test/resources/nonExistingFile.txt");
+    assertThrows(IOException.class, () -> fileHasher.hash(file));
+  }
+
+  @Test
+  void hash_throwsIOExceptionForNullFile() {
+    assertThrows(IOException.class, () -> fileHasher.hash(null));
+  }
+}
\ No newline at end of file
diff --git a/streampipes-pipeline-management/src/test/java/org/apache/streampipes/manager/file/TestFileManager.java b/streampipes-pipeline-management/src/test/java/org/apache/streampipes/manager/file/TestFileManager.java
index a02a309fa3..da62437bc6 100644
--- a/streampipes-pipeline-management/src/test/java/org/apache/streampipes/manager/file/TestFileManager.java
+++ b/streampipes-pipeline-management/src/test/java/org/apache/streampipes/manager/file/TestFileManager.java
@@ -17,34 +17,211 @@
  */
 package org.apache.streampipes.manager.file;
 
+import org.apache.streampipes.commons.file.FileHasher;
+import org.apache.streampipes.model.file.FileMetadata;
+import org.apache.streampipes.storage.api.IFileMetadataStorage;
+
 import org.apache.commons.io.IOUtils;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
 
+import java.io.ByteArrayInputStream;
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.List;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 public class TestFileManager {
 
+  private FileManager fileManager;
+  private IFileMetadataStorage fileMetadataStorage;
+  private FileHandler fileHandler;
+  private FileHasher fileHasher;
+
+
+  private static final String TEST_USER = "testUser";
+
+  @BeforeEach
+  public void setup() {
+    fileMetadataStorage = mock(IFileMetadataStorage.class);
+    fileHandler = mock(FileHandler.class);
+    fileHasher = mock(FileHasher.class);
+    fileManager = new FileManager(fileMetadataStorage, fileHandler, fileHasher);
+  }
+
+  @Test
+  public void getAllFiles_returnsAllFiles() {
+    var expected = prepareFileMetadataStorageWithTwoSampleFiles();
+
+    var result = fileManager.getAllFiles();
+
+    assertEquals(expected, result);
+  }
+
+  @Test
+  public void getAllFiles_returnsAllFilesWhenFiletypesIsNull() {
+    var expected = prepareFileMetadataStorageWithTwoSampleFiles();
+
+    var result = fileManager.getAllFiles(null);
+
+    assertEquals(expected, result);
+  }
+
+  @Test
+  public void getAllFiles_returnsFilteredFilesWhenFiletypesIsNotNull() {
+    var files = prepareFileMetadataStorageWithTwoSampleFiles();
+
+    List<FileMetadata> result = fileManager.getAllFiles("csv");
+
+    assertEquals(1, result.size());
+    assertEquals(files.get(0), result.get(0));
+  }
+
+  @Test
+  public void getAllFiles_returnsEmptyListWhenNoMatchingFiletypes() {
+    prepareFileMetadataStorageWithTwoSampleFiles();
+
+    var result = fileManager.getAllFiles("xml");
+
+    assertEquals(0, result.size());
+  }
+
+  private List<FileMetadata> prepareFileMetadataStorageWithTwoSampleFiles() {
+    List<FileMetadata> allFiles = Arrays.asList(createFileMetadata("csv"), createFileMetadata("json"));
+    when(fileMetadataStorage.getAllFileMetadataDescriptions()).thenReturn(allFiles);
+
+    return allFiles;
+  }
+
+  private FileMetadata createFileMetadata(String fileType) {
+    FileMetadata file = new FileMetadata();
+    file.setFiletype(fileType);
+    return file;
+  }
+
+  @Test
+  public void getFile_returnsExistingFile() {
+    var filename = "existingFile.txt";
+    var expectedFile = new File(filename);
+    when(fileHandler.getFile(filename)).thenReturn(expectedFile);
+
+    var result = fileManager.getFile(filename);
+
+    assertEquals(expectedFile, result);
+  }
+
+  @Test
+  public void getFile_returnsNullForNonExistingFile() {
+    var filename = "fileDoesNotExist.txt";
+    when(fileHandler.getFile(filename)).thenReturn(null);
+
+    assertNull(fileManager.getFile(filename));
+  }
+
   @Test
   public void storeFile_throwsExceptionForInvalidFileType() {
     var filename = "testFile.invalid";
 
     assertThrows(IllegalArgumentException.class, () ->
-        FileManager.storeFile("", filename, mock(InputStream.class)));
+        fileManager.storeFile("", filename, mock(InputStream.class)));
+  }
+
+  @Test
+  public void storeFile_storesFileWithValidInput() throws IOException {
+    var filename = "testFile.csv";
+
+    var fileMetadata = fileManager.storeFile(TEST_USER, filename, mock(InputStream.class));
+
+    assertEquals(TEST_USER, fileMetadata.getCreatedByUser());
+    assertEquals(filename, fileMetadata.getFilename());
+    assertEquals("csv", fileMetadata.getFiletype());
+    verify(fileHandler, times(1)).storeFile(eq(filename), any(InputStream.class));
+    verify(fileMetadataStorage, times(1)).addFileMetadata(any(FileMetadata.class));
+  }
+
+  @Test
+  public void storeFile_sanitizesFilename() throws IOException {
+    var filename = "test@File.csv";
+    var expectedSanitizedFilename = "test_File.csv";
+
+    var fileMetadata = fileManager.storeFile(TEST_USER, filename, mock(InputStream.class));
+
+    assertEquals(expectedSanitizedFilename, fileMetadata.getFilename());
+    verify(fileHandler, times(1)).storeFile(eq(expectedSanitizedFilename), any(InputStream.class));
+    verify(fileMetadataStorage, times(1)).addFileMetadata(any(FileMetadata.class));
+  }
+
+  /**
+   * This test validates that the storeFile method removes the BOM from the file before it is stored.
+   */
+  @Test
+  public void storeFile_removesBom() throws IOException {
+    var expectedContent = "test content";
+    // prepare input stream with BOM
+    var fileInputStream = new ByteArrayInputStream(("\uFEFF" + expectedContent)
+                                                       .getBytes(StandardCharsets.UTF_8));
+
+    fileManager.storeFile(TEST_USER, "testfile.csv", fileInputStream);
+
+    var inputStreamCaptor = ArgumentCaptor.forClass(InputStream.class);
+    verify(fileHandler, times(1)).storeFile(any(), inputStreamCaptor.capture());
+
+    // Convert the captured InputStream to a String
+    var capturedInputStream = inputStreamCaptor.getValue();
+    var capturedContent = IOUtils.toString(capturedInputStream, StandardCharsets.UTF_8);
+
+    // Assert that the captured content is equal to the expected content
+    assertEquals(expectedContent, capturedContent);
+  }
+
+
+  @Test
+  public void deleteFile_removesExistingFile() {
+    var id = "existingFileId";
+    var fileMetadata = new FileMetadata();
+    fileMetadata.setFilename("existingFile.txt");
+
+    when(fileMetadataStorage.getMetadataById(id)).thenReturn(fileMetadata);
+
+    fileManager.deleteFile(id);
+
+    verify(fileHandler, times(1)).deleteFile(fileMetadata.getFilename());
+    verify(fileMetadataStorage, times(1)).deleteFileMetadata(id);
+  }
+
+  @Test
+  public void deleteFile_doesNothingForNonExistingFile() {
+    var id = "nonExistingFileId";
+
+    when(fileMetadataStorage.getMetadataById(id)).thenReturn(null);
+
+    fileManager.deleteFile(id);
+
+    verify(fileHandler, times(0)).deleteFile(anyString());
+    verify(fileMetadataStorage, times(0)).deleteFileMetadata(id);
   }
 
   @Test
   public void testCleanFileWithoutBom() throws IOException {
     var expected = "test";
     var inputStream = IOUtils.toInputStream(expected, StandardCharsets.UTF_8);
-    var resultStream = FileManager.cleanFile(inputStream, "CSV");
+    var resultStream = fileManager.cleanFile(inputStream, "CSV");
     var resultString = IOUtils.toString(resultStream, StandardCharsets.UTF_8);
 
     assertEquals(expected, resultString);
@@ -56,7 +233,7 @@ public class TestFileManager {
     var utf8Bom = "\uFEFF";
     var inputString = utf8Bom + expected;
     var inputStream = IOUtils.toInputStream(inputString, StandardCharsets.UTF_8);
-    var resultStream = FileManager.cleanFile(inputStream, "CSV");
+    var resultStream = fileManager.cleanFile(inputStream, "CSV");
     var resultString = IOUtils.toString(resultStream, StandardCharsets.UTF_8);
 
     assertEquals(expected, resultString);
@@ -68,7 +245,7 @@ public class TestFileManager {
     var utf8Bom = "\uFEFF";
     var inputString = utf8Bom + expected;
     var inputStream = IOUtils.toInputStream(inputString, StandardCharsets.UTF_8);
-    var resultStream = FileManager.cleanFile(inputStream, "CSV");
+    var resultStream = fileManager.cleanFile(inputStream, "CSV");
     var resultString = IOUtils.toString(resultStream, StandardCharsets.UTF_8);
 
     assertEquals(expected, resultString);
@@ -77,57 +254,85 @@ public class TestFileManager {
   @Test
   public void sanitizeFilename_replacesNonAlphanumericCharactersWithUnderscore() {
     var filename = "file@name#with$special%characters";
-    var sanitizedFilename = FileManager.sanitizeFilename(filename);
+    var sanitizedFilename = fileManager.sanitizeFilename(filename);
     assertEquals("file_name_with_special_characters", sanitizedFilename);
   }
 
   @Test
   public void sanitizeFilename_keepsAlphanumericAndDotAndHyphenCharacters() {
     var filename = "file.name-with_alphanumeric123";
-    var sanitizedFilename = FileManager.sanitizeFilename(filename);
+    var sanitizedFilename = fileManager.sanitizeFilename(filename);
     assertEquals(filename, sanitizedFilename);
   }
 
   @Test
   public void sanitizeFilename_returnsUnderscoreForFilenameWithAllSpecialCharacters() {
     var filename = "@#$%^&*()";
-    var sanitizedFilename = FileManager.sanitizeFilename(filename);
+    var sanitizedFilename = fileManager.sanitizeFilename(filename);
     assertEquals("_________", sanitizedFilename);
   }
 
   @Test
   public void sanitizeFilename_returnsEmptyStringForEmptyFilename() {
     var filename = "";
-    var sanitizedFilename = FileManager.sanitizeFilename(filename);
+    var sanitizedFilename = fileManager.sanitizeFilename(filename);
     assertEquals("", sanitizedFilename);
   }
 
   @Test
   public void sanitizeFilename_removesSingleParentDirectory() {
     var filename = "../file.csv";
-    var sanitizedFilename = FileManager.sanitizeFilename(filename);
+    var sanitizedFilename = fileManager.sanitizeFilename(filename);
     assertEquals(".._file.csv", sanitizedFilename);
   }
 
   @Test
   public void sanitizeFilename_removesDoubleParentDirectoryy() {
     var filename = "../../file";
-    var sanitizedFilename = FileManager.sanitizeFilename(filename);
+    var sanitizedFilename = fileManager.sanitizeFilename(filename);
     assertEquals(".._.._file", sanitizedFilename);
   }
 
   @Test
   public void validateFileName_returnsTrueForCsv() {
-    assertTrue(FileManager.validateFileType("file.csv"));
+    assertTrue(fileManager.validateFileType("file.csv"));
   }
 
   @Test
   public void validateFileName_returnsTrueForJson() {
-    assertTrue(FileManager.validateFileType("file.json"));
+    assertTrue(fileManager.validateFileType("file.json"));
   }
 
   @Test
   public void validateFileName_returnsFalseForSh() {
-    assertFalse(FileManager.validateFileType("file.sh"));
+    assertFalse(fileManager.validateFileType("file.sh"));
+  }
+
+  @Test
+  public void checkFileContentChanged_returnsTrueWhenContentHasChanged() throws IOException {
+    var filename = "testFile.csv";
+    var originalHash = "originalHash";
+    var changedHash = "changedHash";
+
+    when(fileHandler.getFile(filename)).thenReturn(new File(filename));
+    when(fileHasher.hash(any(File.class))).thenReturn(changedHash);
+
+    var result = fileManager.checkFileContentChanged(filename, originalHash);
+
+    assertTrue(result);
+  }
+
+  @Test
+  public void checkFileContentChanged_returnsFalseWhenContentHasNotChanged() throws IOException {
+    var filename = "testFile.csv";
+    var originalHash = "originalHash";
+
+    when(fileHandler.getFile(filename)).thenReturn(new File(filename));
+    when(fileHasher.hash(any(File.class))).thenReturn(originalHash);
+
+    var result = fileManager.checkFileContentChanged(filename, originalHash);
+
+    assertFalse(result);
   }
+
 }
\ No newline at end of file