diff --git a/app_storage/metadata/metadata.temp b/app_storage/metadata/metadata.temp
index ca44c66..124a382 100644
--- a/app_storage/metadata/metadata.temp
+++ b/app_storage/metadata/metadata.temp
@@ -1 +1 @@
-#Wed Oct 15 22:29:09 CEST 2025
+#Thu Oct 30 19:28:32 CET 2025
diff --git a/options/app.config b/options/app.config
index 8ba896f..61aeeb3 100644
--- a/options/app.config
+++ b/options/app.config
@@ -1,4 +1,5 @@
-#Sat Oct 18 15:00:03 CEST 2025
+#Sat Nov 01 14:24:19 CET 2025
frontend.licensekey=
frontend.splashscreen=false
frontend.theme=1
+scanner.max_hash_extraction_file_size=-1
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 432db6f..6a6cbf1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -8,7 +8,7 @@
org.pwssintegrity_hashjar
- 1.0
+ 1.1A GUI application for file integrity checking
@@ -60,25 +60,25 @@
-
+
com.fasterxml.jackson.corejackson-databind
- 2.20.0
+ 2.20.1ch.qos.logbacklogback-classic
- 1.5.20
+ 1.5.21org.junit.jupiterjunit-jupiter-api
- 6.0.0
+ 6.0.1test
@@ -90,11 +90,11 @@
-
+
com.formdevflatlaf
- 3.6.1
+ 3.6.2
@@ -103,10 +103,10 @@
-
+
org.codehaus.mojoexec-maven-plugin
- 3.5.1
+ 3.6.2${project.mainClass}
@@ -114,10 +114,10 @@
-
+
org.apache.maven.pluginsmaven-compiler-plugin
- 3.14.0
+ 3.14.1${maven.compiler.source}${maven.compiler.target}
@@ -126,7 +126,7 @@
-
+
org.apache.maven.pluginsmaven-assembly-plugin3.7.1
diff --git a/src/main/java/org/pwss/FileIntegrityScannerFrontend.java b/src/main/java/org/pwss/FileIntegrityScannerFrontend.java
index e8deb54..72fd5d3 100644
--- a/src/main/java/org/pwss/FileIntegrityScannerFrontend.java
+++ b/src/main/java/org/pwss/FileIntegrityScannerFrontend.java
@@ -16,7 +16,7 @@
import org.pwss.navigation.NavigationEvents;
import org.pwss.navigation.NavigationHandler;
import org.pwss.navigation.Screen;
-import org.pwss.utils.OSUtils;
+import org.pwss.util.OSUtil;
import org.pwss.view.screen.splash_screen.FileIntegrityScannerSplashScreen;
import org.slf4j.LoggerFactory;
@@ -40,7 +40,7 @@ class FileIntegrityScannerFrontend {
final static void StartApplication() throws FailedToLaunchAppException {
final org.slf4j.Logger log = LoggerFactory.getLogger(FileIntegrityScannerFrontend.class);
log.debug("Starting File-Integrity Scanner Frontend Application");
- log.debug("OS Name: {}", OSUtils.getOSName());
+ log.debug("OS Name: {}", OSUtil.getOSName());
try {
if (AppConfig.USE_SPLASH_SCREEN) {
FileIntegrityScannerSplashScreen.showSplash();
diff --git a/src/main/java/org/pwss/app_settings/AppConfig.java b/src/main/java/org/pwss/app_settings/AppConfig.java
index c967c4d..22f2e26 100644
--- a/src/main/java/org/pwss/app_settings/AppConfig.java
+++ b/src/main/java/org/pwss/app_settings/AppConfig.java
@@ -27,6 +27,12 @@ public final class AppConfig {
* when the class is initialized.
*/
public final static String LICENSE_KEY;
+ /**
+ * Maximum file size (in bytes) for hash extraction. This value is loaded from
+ * the configuration file
+ * when the class is initialized.
+ */
+ public final static long MAX_HASH_EXTRACTION_FILE_SIZE;
/**
* ConfigLoader instance used to load and manage configuration values.
@@ -41,6 +47,7 @@ public final class AppConfig {
USE_SPLASH_SCREEN = configLoader.isUseSplashScreen();
APP_THEME = configLoader.getAppTheme();
LICENSE_KEY = configLoader.getLicenseKey();
+ MAX_HASH_EXTRACTION_FILE_SIZE = configLoader.getHashExtractionMaxFileSizeValue();
}
/**
@@ -80,4 +87,16 @@ public static final boolean setLicenseKey(String licenseKey) {
return configLoader.setLicenseKey(licenseKey);
}
+ /**
+ * Sets the maximum file size for hash extraction in the configuration file.
+ * This change will take effect only after
+ * the frontend application is restarted.
+ *
+ * @param maxFileSize The value to set for the maximum file size (in bytes)
+ * @return true if setting was successful, otherwise false
+ */
+ public static final boolean setMaxHashExtractionFileSize(long maxFileSize) {
+ return configLoader.setMaxHashExtractionFileSize(String.valueOf(maxFileSize));
+ }
+
}
diff --git a/src/main/java/org/pwss/app_settings/ConfigLoader.java b/src/main/java/org/pwss/app_settings/ConfigLoader.java
index 5ced67d..859fb9d 100644
--- a/src/main/java/org/pwss/app_settings/ConfigLoader.java
+++ b/src/main/java/org/pwss/app_settings/ConfigLoader.java
@@ -29,6 +29,10 @@ final class ConfigLoader {
* Key in the properties file for license key setting.
*/
private final String LICENSE_KEY = "frontend.licensekey";
+ /**
+ * Key in the properties file for maximum hash extraction file size setting.
+ */
+ private final String MAX_HASH_EXTRACTION_FILE_SIZE_KEY = "scanner.max_hash_extraction_file_size";
/**
* Path to the configuration file.
@@ -52,6 +56,10 @@ final class ConfigLoader {
* License key for the application.
*/
private final String licenseKey;
+ /**
+ * Maximum hash extraction file size.
+ */
+ private final long maxHashExtractionFileSize;
/**
* Constructor that loads configuration settings from the properties file and
@@ -67,10 +75,12 @@ final class ConfigLoader {
this.useSplashScreen = true;
this.appTheme = 1;
this.licenseKey = "none";
+ this.maxHashExtractionFileSize = -1;
} else {
this.useSplashScreen = getSplashScreenFlagFromConfigString(getSplashScreenProperty());
this.appTheme = getAppThemeValueFromConfigString(getAppThemeProperty());
this.licenseKey = getLicenseKeyProperty();
+ this.maxHashExtractionFileSize = getMaxHashExtractionFileSizeFromConfigString(getMaxHashExtractionFileSizeProperty());
}
}
@@ -79,7 +89,7 @@ final class ConfigLoader {
* Parses the splash screen flag from a configuration string.
*
* @param configFileString The configuration string to be parsed
- * @return falsee if the string is "false" (case insensitive), otherwise true
+ * @return false if the string is "false" (case insensitive), otherwise true
*/
private final boolean getSplashScreenFlagFromConfigString(String configFileString) {
@@ -124,6 +134,29 @@ private final int getAppThemeValueFromConfigString(String configFileString) {
}
+ /**
+ * Parses the maximum hash extraction file size from a configuration string.
+ *
+ * @return The long value of the maximum hash extraction file size, or -1 if
+ * parsing fails or the value is invalid
+ */
+ private final long getMaxHashExtractionFileSizeFromConfigString(String configFileString) {
+ try {
+ long maxHashExtractionFileSizeValue = Long.parseLong(configFileString);
+
+ if (maxHashExtractionFileSizeValue >= -1)
+ return maxHashExtractionFileSizeValue;
+ else
+ return -1;
+ }
+
+ catch (Exception exception) {
+ log.debug("Could not parse max hash extraction file size value from app settings", exception);
+ log.error("Could not parse max hash extraction file size value from app settings {}", exception.getMessage());
+ return -1;
+ }
+ }
+
/**
* Loads the configuration properties from the file specified by
* CONFIG_FILE_PATH.
@@ -200,6 +233,23 @@ private final String getLicenseKeyProperty() {
}
+ /**
+ * Retrieves the maximum hash extraction file size property value from the
+ * properties file.
+ *
+ * @return The maximum hash extraction file size property value, or "-1" if an
+ * error occurs
+ */
+ private final String getMaxHashExtractionFileSizeProperty() {
+ try {
+ return properties.getProperty(MAX_HASH_EXTRACTION_FILE_SIZE_KEY);
+ } catch (Exception exception) {
+ log.debug("Max hash extraction file size property could not be fetched", exception);
+ log.error("Max hash extraction file size property could not be fetched {}", exception.getMessage());
+ return "-1";
+ }
+ }
+
/**
* Sets the splash screen flag in the properties file.
*
@@ -260,6 +310,27 @@ final boolean setLicenseKey(String licenseKey) {
}
}
+ /**
+ * Sets the maximum hash extraction file size value in the properties file.
+ *
+ * @param maxHashExtractionFileSize The value to set for the maximum hash
+ * extraction file size
+ * @return true if setting was successful, otherwise false
+ */
+ final boolean setMaxHashExtractionFileSize(String maxHashExtractionFileSize) {
+
+ properties.setProperty(MAX_HASH_EXTRACTION_FILE_SIZE_KEY, maxHashExtractionFileSize);
+
+ try (FileOutputStream fos = new FileOutputStream(CONFIG_FILE_PATH)) {
+ properties.store(fos, null);
+ return true;
+ } catch (Exception exception) {
+ log.debug("Max hash extraction file size could not be set in app.config file", exception);
+ log.error("Max hash extraction file size could not be set in app.config file: {}", exception.getMessage());
+ return false;
+ }
+ }
+
/**
* Gets the value indicating whether to use splash screen or not.
*
@@ -286,4 +357,8 @@ final int getAppTheme() {
final String getLicenseKey() {
return licenseKey;
}
+
+ final long getHashExtractionMaxFileSizeValue() {
+ return maxHashExtractionFileSize;
+ }
}
\ No newline at end of file
diff --git a/src/main/java/org/pwss/controller/BaseController.java b/src/main/java/org/pwss/controller/BaseController.java
index 580d966..2e632b0 100644
--- a/src/main/java/org/pwss/controller/BaseController.java
+++ b/src/main/java/org/pwss/controller/BaseController.java
@@ -16,12 +16,14 @@ public abstract class BaseController {
* Represents the UI component associated with this controller.
*/
protected Screen screen;
-
/**
* The navigation context for passing data between different parts of the application during navigation.
*/
private NavigationContext context;
-
+ /**
+ * Logger instance for logging purposes.
+ */
+ private final org.slf4j.Logger log;
/**
* Constructs a `BaseController` with the specified view.
* Initializes the view and sets up event listeners.
@@ -30,12 +32,12 @@ public abstract class BaseController {
*/
public BaseController(Screen screen) {
this.screen = screen;
+ this.log = org.slf4j.LoggerFactory.getLogger(BaseController.class);
// Run onCreate lifecycle method
onCreate();
// Initialize event listeners
initListeners();
}
-
/**
* Retrieves the current navigation context.
*
@@ -44,7 +46,6 @@ public BaseController(Screen screen) {
protected NavigationContext getContext() {
return context;
}
-
/**
* Sets the navigation context for the controller.
*
@@ -53,26 +54,22 @@ protected NavigationContext getContext() {
public void setContext(NavigationContext context) {
this.context = context;
}
-
/**
* Abstract method to initialize event listeners for the Screen.
* Subclasses must provide an implementation for this method.
*/
abstract void initListeners();
-
/**
* Abstract method to refresh or update the view.
* Subclasses must provide an implementation for this method.
*/
abstract void refreshView();
-
/**
* Method to reload or refresh data displayed in the view.
* Subclasses can override this method to provide specific data reloading logic.
- * The default implementation does nothing.
*/
public void reloadData() {
- // Default implementation does nothing
+ log.debug("reloadData called for {}", screen.getScreenName());
}
/**
@@ -83,22 +80,19 @@ public void reloadData() {
public Screen getScreen() {
return screen;
}
-
/**
* Method called when the view is shown.
* Subclasses can override this method to perform actions when the view becomes visible.
- * The default implementation does nothing.
*/
public void onShow() {
- // Default implementation does nothing
+ log.debug("onShow called for {}", screen.getScreenName());
}
/**
* Method called when the view is created.
* Subclasses can override this method to perform actions during the creation of the view.
- * The default implementation does nothing.
*/
public void onCreate() {
- // Default implementation does nothing
+ log.debug("onCreate called for {}", screen.getScreenName());
}
}
diff --git a/src/main/java/org/pwss/controller/HomeController.java b/src/main/java/org/pwss/controller/HomeController.java
index 6482af7..61d761c 100644
--- a/src/main/java/org/pwss/controller/HomeController.java
+++ b/src/main/java/org/pwss/controller/HomeController.java
@@ -1,6 +1,7 @@
package org.pwss.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
+
import java.awt.Color;
import java.awt.Component;
import java.awt.event.ItemEvent;
@@ -13,7 +14,6 @@
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicLong;
-
import javax.swing.DefaultListCellRenderer;
import javax.swing.DefaultListModel;
import javax.swing.JList;
@@ -25,9 +25,11 @@
import javax.swing.Timer;
import org.pwss.app_settings.AppConfig;
import org.pwss.controller.util.NavigationContext;
+import org.pwss.data_structure.RingBuffer;
import org.pwss.exception.metadata.MetadataKeyNameRetrievalException;
import org.pwss.exception.monitored_directory.MonitoredDirectoryGetAllException;
import org.pwss.exception.scan.GetAllMostRecentScansException;
+import org.pwss.exception.scan.GetDiffCountException;
import org.pwss.exception.scan.GetMostRecentScansException;
import org.pwss.exception.scan.GetScanDiffsException;
import org.pwss.exception.scan.LiveFeedException;
@@ -60,21 +62,24 @@
import org.pwss.service.NoteService;
import org.pwss.service.ScanService;
import org.pwss.service.ScanSummaryService;
-import org.pwss.utils.AppTheme;
-import org.pwss.utils.LiveFeedUtils;
-import org.pwss.utils.MonitoredDirectoryUtils;
-import org.pwss.utils.OSUtils;
-import org.pwss.utils.ReportUtils;
-import org.pwss.utils.StringConstants;
+import org.pwss.util.AppTheme;
+import org.pwss.util.ConversionUtil;
+import org.pwss.util.ErrorUtil;
+import org.pwss.util.LiveFeedUtil;
+import org.pwss.util.MonitoredDirectoryUtil;
+import org.pwss.util.OSUtil;
+import org.pwss.util.ReportUtil;
+import org.pwss.util.ScanUtil;
+import org.pwss.util.StringConstants;
import org.pwss.view.popup_menu.MonitoredDirectoryPopupFactory;
import org.pwss.view.popup_menu.listener.MonitoredDirectoryPopupListenerImpl;
import org.pwss.view.screen.HomeScreen;
import org.slf4j.LoggerFactory;
import static org.pwss.app_settings.AppConfig.APP_THEME;
+import static org.pwss.app_settings.AppConfig.MAX_HASH_EXTRACTION_FILE_SIZE;
import static org.pwss.app_settings.AppConfig.USE_SPLASH_SCREEN;
-// TODO: NEEDS REFACTORING - VERY LARGE CLASS
public final class HomeController extends BaseController {
/**
@@ -127,6 +132,11 @@ public final class HomeController extends BaseController {
*/
private List recentScans;
+ /**
+ * Count of differences found in recent scans.
+ */
+ private int recentDiffsCount;
+
/**
* List of most recent differences detected in the scans.
*/
@@ -167,6 +177,11 @@ public final class HomeController extends BaseController {
*/
private boolean showSplashScreenSetting;
+ /**
+ * Maximum file size (in bytes) for which hash extraction will be performed.
+ */
+ private long maxFileSizeForHashExtraction;
+
/**
* Constructor to initialize HomeController with a HomeScreen view instance.
*
@@ -183,10 +198,12 @@ public HomeController(HomeScreen view) {
this.monitoredDirectoryPopupFactory = new MonitoredDirectoryPopupFactory(
new MonitoredDirectoryPopupListenerImpl(this, monitoredDirectoryService, noteService));
this.showSplashScreenSetting = USE_SPLASH_SCREEN;
+ this.maxFileSizeForHashExtraction = MAX_HASH_EXTRACTION_FILE_SIZE;
}
@Override
public void onCreate() {
+ super.onCreate();
// Update theme picker
screen.getThemePicker().removeAllItems();
// Populate the combo box with AppTheme values
@@ -228,6 +245,7 @@ public Component getListCellRendererComponent(JList> list, Object value,
@Override
public void onShow() {
+ super.onShow();
fetchDataAndRefreshView();
}
@@ -240,17 +258,28 @@ void fetchDataAndRefreshView() {
try {
// Fetch all monitored directories for display in the monitored directories
// table
- allMonitoredDirectories = monitoredDirectoryService.getAllDirectories();
+ allMonitoredDirectories = MonitoredDirectoryUtil
+ .filterMonitoredDirectoriesOnConfirmedPath(monitoredDirectoryService.getAllDirectories());
- // Only fetch diffs if there are monitored directories present
- if (!allMonitoredDirectories.isEmpty()) {
+ // Only fetch diffs if there are active monitored directories present
+ final long activeDirCount = allMonitoredDirectories.stream().filter(MonitoredDirectory::isActive).count();
+ if (activeDirCount > 0) {
// Fetch recent scans for display in the scan table
recentScans = scanService.getMostRecentScansAll();
if (recentScans.isEmpty()) {
recentDiffs = List.of();
} else {
+ List distinctRecentScans = ScanUtil.getScansDistinctByDirectory(recentScans);
+
+ // Calculate diff count for the recent scans
+ recentDiffsCount = 0;
+ for (Scan scan : distinctRecentScans) {
+ int count = scanService.getScanDiffsCount(scan.id());
+ recentDiffsCount += count;
+ }
+
// Fetch diffs for the most recent scan to show in the diffs table
- recentDiffs = recentScans.stream()
+ recentDiffs = distinctRecentScans.stream()
.flatMap(scan -> safeGetDiffs(scan.id()).stream())
.toList();
}
@@ -268,8 +297,8 @@ void fetchDataAndRefreshView() {
scanRunning = true;
startPollingScanLiveFeed(false, Collections.emptyList());
}
- } catch (MonitoredDirectoryGetAllException | ExecutionException | InterruptedException | JsonProcessingException
- | GetAllMostRecentScansException | ScanStatusException e) {
+ } catch (MonitoredDirectoryGetAllException | ExecutionException | InterruptedException |
+ JsonProcessingException | GetAllMostRecentScansException | ScanStatusException | GetDiffCountException e) {
log.error("Error getting data: {}", e.getMessage());
SwingUtilities.invokeLater(() -> screen.showError("Error getting data"));
}
@@ -287,7 +316,7 @@ void fetchDataAndRefreshView() {
*/
private List safeGetDiffs(long scanId) {
try {
- return scanService.getDiffs(scanId, 1000, null, false);
+ return scanService.getDiffs(scanId, (Integer.MAX_VALUE -1), null, false);
} catch (GetScanDiffsException | ExecutionException | InterruptedException | JsonProcessingException e) {
return List.of();
}
@@ -295,6 +324,7 @@ private List safeGetDiffs(long scanId) {
@Override
public void reloadData() {
+ super.reloadData();
fetchDataAndRefreshView();
}
@@ -359,7 +389,7 @@ public void mouseClicked(MouseEvent e) {
DiffTableModel model = (DiffTableModel) screen.getDiffTable().getModel();
Optional diff = model.getDiffAt(modelRow);
- diff.ifPresent(d -> screen.getDiffDetails().setText(ReportUtils.formatDiff(d)));
+ diff.ifPresent(d -> screen.getDiffDetails().setText(ReportUtil.formatDiff(d)));
} else {
screen.getDiffDetails().setText("");
}
@@ -383,7 +413,7 @@ public void mouseClicked(MouseEvent e) {
int selectedRow = screen.getFileScanSummaryTable().getSelectedRow();
if (selectedRow >= 0 && selectedRow < fileSummaries.size()) {
ScanSummary selectedSummary = fileSummaries.get(selectedRow);
- screen.getScanSummaryDetails().setText(ReportUtils.formatSummary(selectedSummary));
+ screen.getScanSummaryDetails().setText(ReportUtil.formatSummary(selectedSummary));
} else {
screen.getScanSummaryDetails().setText("");
}
@@ -411,6 +441,45 @@ public void mouseClicked(MouseEvent e) {
}
});
+ screen.getMaxHashExtractionFileSizeSlider().addChangeListener(l -> {
+ long valueMegabytes = screen.getMaxHashExtractionFileSizeSlider().getValue();
+ log.debug("Setting max hash extraction file size to {} MB", valueMegabytes);
+ long valueBytes = ConversionUtil.megabytesToBytes(valueMegabytes);
+
+ // Set App config value to size in bytes
+ if (AppConfig.setMaxHashExtractionFileSize(valueBytes)) {
+ screen.getMaxHashExtractionFileSizeValueLabel().setText(valueMegabytes + " MB");
+ maxFileSizeForHashExtraction = ConversionUtil.megabytesToBytes(valueMegabytes);
+ } else {
+ log.error("Failed to update max hash extraction file size in app config.");
+ }
+ });
+ screen.getMaxHashExtractionFileSizeUnlimitedCheckbox().addActionListener(l -> {
+ // If checked set the max size to -1L
+ boolean checked = screen.getMaxHashExtractionFileSizeUnlimitedCheckbox().isSelected();
+ if (checked) {
+ log.debug("Setting max hash extraction file size to unlimited.");
+ if (AppConfig.setMaxHashExtractionFileSize(-1L)) {
+ maxFileSizeForHashExtraction = -1L;
+ screen.getMaxHashExtractionFileSizeValueLabel().setText("Unlimited");
+ screen.getMaxHashExtractionFileSizeSlider().setEnabled(false);
+ } else {
+ log.error("Failed to update max hash extraction file size in app config.");
+ }
+ } else {
+ // If unchecked set the size to the current slider value
+ long sliderValueMegabytes = screen.getMaxHashExtractionFileSizeSlider().getValue();
+ log.debug("Setting max hash extraction file size to {} MB", sliderValueMegabytes);
+ long sliderValueBytes = ConversionUtil.megabytesToBytes(sliderValueMegabytes);
+ if (AppConfig.setMaxHashExtractionFileSize(sliderValueBytes)) {
+ maxFileSizeForHashExtraction = sliderValueBytes;
+ screen.getMaxHashExtractionFileSizeValueLabel().setText(sliderValueMegabytes + " MB");
+ screen.getMaxHashExtractionFileSizeSlider().setEnabled(true);
+ } else {
+ log.error("Failed to update max hash extraction file size in app config.");
+ }
+ }
+ });
}
@Override
@@ -418,7 +487,7 @@ protected void refreshView() {
// Update UI components based on the current state
screen.getScanButton().setText(scanRunning ? StringConstants.SCAN_STOP : StringConstants.SCAN_FULL);
- String notifications = MonitoredDirectoryUtils
+ String notifications = MonitoredDirectoryUtil
.getMonitoredDirectoryNotificationMessage(allMonitoredDirectories);
boolean hasNotifications = !notifications.isEmpty();
@@ -464,7 +533,7 @@ public Component getListCellRendererComponent(JList> list, Object value, int i
if (!dir.baselineEstablished()) {
setForeground(Color.YELLOW);
setToolTipText(StringConstants.TOOLTIP_BASELINE_NOT_ESTABLISHED);
- } else if (MonitoredDirectoryUtils.isScanOlderThanAWeek(dir)) {
+ } else if (MonitoredDirectoryUtil.isScanOlderThanAWeek(dir)) {
setForeground(Color.ORANGE);
setToolTipText(StringConstants.TOOLTIP_OLD_SCAN);
} else {
@@ -480,6 +549,9 @@ public Component getListCellRendererComponent(JList> list, Object value, int i
screen.getMonitoredDirectoriesTable().setModel(monitoredDirectoryTableModel);
screen.getMonitoredDirectoriesTable().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+ // Update diffs count label
+ screen.getDiffsCountLabel().setText("Diffs found: " + recentDiffsCount);
+
DiffTableModel diffTableModel = new DiffTableModel(recentDiffs != null ? recentDiffs : List.of());
screen.getDiffTable().setModel(diffTableModel);
screen.getDiffTable().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
@@ -492,7 +564,7 @@ public Component getListCellRendererComponent(JList> list, Object value, int i
diff.ifPresent(d -> {
int choice = screen.showOptionDialog(
JOptionPane.WARNING_MESSAGE,
- OSUtils.getQuarantineWarningMessage(),
+ OSUtil.getQuarantineWarningMessage(),
new String[] { StringConstants.GENERIC_YES, StringConstants.GENERIC_NO },
StringConstants.GENERIC_NO);
if (choice == 0) {
@@ -550,6 +622,20 @@ public Component getListCellRendererComponent(JList> list, Object value, int i
}
});
}));
+
+ if (maxFileSizeForHashExtraction != -1L) {
+ screen.getMaxHashExtractionFileSizeUnlimitedCheckbox().setSelected(false);
+ screen.getMaxHashExtractionFileSizeSlider().setEnabled(true);
+
+ final int maxSliderValueMegabytes = Math
+ .toIntExact(ConversionUtil.bytesToMegabytes(maxFileSizeForHashExtraction));
+ screen.getMaxHashExtractionFileSizeSlider().setValue(maxSliderValueMegabytes);
+ screen.getMaxHashExtractionFileSizeValueLabel().setText(maxSliderValueMegabytes + " MB");
+ } else {
+ screen.getMaxHashExtractionFileSizeUnlimitedCheckbox().setSelected(true);
+ screen.getMaxHashExtractionFileSizeSlider().setEnabled(false);
+ screen.getMaxHashExtractionFileSizeValueLabel().setText("Unlimited");
+ }
}
/**
@@ -595,7 +681,7 @@ public void performStartScan(boolean singleDirectory) {
int modelRow = table.convertRowIndexToModel(viewRow);
Optional dir = model.getDirectoryAt(modelRow);
if (dir.isPresent()) {
- startScanSuccess = scanService.startScanById(dir.get().id());
+ startScanSuccess = scanService.startScanById(dir.get().id(), maxFileSizeForHashExtraction);
scanningDirs.add(dir.get());
baseLineScan = !dir.get().baselineEstablished();
} else {
@@ -605,7 +691,7 @@ public void performStartScan(boolean singleDirectory) {
}
} else {
baseLineScan = false;
- startScanSuccess = scanService.startScan();
+ startScanSuccess = scanService.startScan(maxFileSizeForHashExtraction);
scanningDirs.addAll(allMonitoredDirectories);
}
SwingUtilities.invokeLater(() -> {
@@ -614,7 +700,7 @@ public void performStartScan(boolean singleDirectory) {
if (baseLineScan) {
screen.showSuccess(StringConstants.SCAN_STARTED_BASELINE_SUCCESS);
} else {
- screen.showSuccess(StringConstants.SCAN_STARTED_SUCCESS);
+ log.info(StringConstants.SCAN_STARTED_SUCCESS);
}
startPollingScanLiveFeed(singleDirectory, scanningDirs);
} else {
@@ -625,7 +711,8 @@ public void performStartScan(boolean singleDirectory) {
| JsonProcessingException e) {
log.debug(StringConstants.SCAN_START_ERROR, e);
log.error(StringConstants.SCAN_START_ERROR + " {}", e.getMessage());
- SwingUtilities.invokeLater(() -> screen.showError(StringConstants.SCAN_START_ERROR));
+ SwingUtilities.invokeLater(() -> screen
+ .showError(ErrorUtil.formatErrorMessage(StringConstants.SCAN_START_ERROR, e.getMessage())));
}
}
@@ -657,7 +744,8 @@ private void onFinishScan(boolean completed, boolean singleDirectory) {
int choice;
// Prompt the user to view scan results based on whether differences were found
if (totalDiffCount.get() > 0) {
- choice = screen.showOptionDialog(JOptionPane.WARNING_MESSAGE, StringConstants.SCAN_COMPLETED_DIFFS,
+ choice = screen.showOptionDialog(JOptionPane.WARNING_MESSAGE,
+ ScanUtil.constructDiffMessageString(totalDiffCount.get()),
new String[] { StringConstants.GENERIC_YES, StringConstants.GENERIC_NO },
StringConstants.GENERIC_YES);
} else {
@@ -706,7 +794,7 @@ private void onFinishScan(boolean completed, boolean singleDirectory) {
if (totalDiffCount.get() > 0) {
// If full scan, and we have diffs, navigate to the diffs tab to show all
// differences.
- screen.getTabbedPane().setSelectedIndex(2);
+ screen.getTabbedPane().setSelectedIndex(3);
} else {
// If no diffs, navigate to the recent scans tab to show the most recent scan.
screen.getTabbedPane().setSelectedIndex(0);
@@ -758,42 +846,70 @@ private void startPollingScanLiveFeed(boolean singleDirectory, List liveFeedBuffer = new RingBuffer<>(100);
+
+ // Create and start a polling timer
scanStatusTimer = new Timer(1000, e -> {
try {
- // Retrieve live feed updates
+ // Get live feed from backend
LiveFeedResponse liveFeed = scanService.getLiveFeed();
- String currentLiveFeedText = screen.getLiveFeedText().getText();
- String newEntry = LiveFeedUtils.formatLiveFeedEntry(liveFeed.livefeed());
- String updatedLiveFeedText = currentLiveFeedText + newEntry;
- screen.getLiveFeedText().setText(updatedLiveFeedText);
+ // Format the new update from the backend
+ String newEntry = LiveFeedUtil.formatLiveFeedEntry(liveFeed.livefeed());
+
+ // Add the new update to the ring buffer
+ liveFeedBuffer.add(newEntry);
+
+ // Build the text for the live feed based on the latest updates in the buffer
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < liveFeedBuffer.size(); i++) {
+ sb.append(liveFeedBuffer.get(i));
+ }
- // Update the total difference count based on new warnings
- totalDiffCount.addAndGet(LiveFeedUtils.countWarnings(liveFeed.livefeed()));
+ // Update UI with the latest text
+ screen.getLiveFeedText().setText(sb.toString());
+
+ // Update the diff counter
+ totalDiffCount.addAndGet(LiveFeedUtil.countWarnings(liveFeed.livefeed()));
screen.getLiveFeedDiffCount().setText(StringConstants.SCAN_DIFFS_PREFIX + totalDiffCount);
- // Update scanRunning state and refresh the UI if necessary
+ // Update scan status and UI as needed
if (liveFeed.isScanRunning() != scanRunning) {
scanRunning = liveFeed.isScanRunning();
refreshView();
}
+
+ // When the scan is complete, do a final pull
if (!liveFeed.isScanRunning()) {
+ log.debug("Scan completed, making final pull...");
- scanStatusTimer.stop(); // Terminate polling when the scan completes
+ // Do a final pull without time pressure
+ liveFeed = scanService.getLiveFeed(); // The last pull to get any final updates
- liveFeed = scanService.getLiveFeed();
+ // Add the last update to the buffer
+ newEntry = LiveFeedUtil.formatLiveFeedEntry(liveFeed.livefeed());
+ if (!org.pwss.util.StringUtil.isEmpty(newEntry)) {
+ liveFeedBuffer.add(newEntry); // Add the last pull to the buffer
- totalDiffCount.getAndAdd(LiveFeedUtils.countWarnings(liveFeed.livefeed()));
- screen.getLiveFeedDiffCount().setText(StringConstants.SCAN_DIFFS_PREFIX + totalDiffCount);
+ // Update UI with the last text
+ sb.setLength(0); // Clear StringBuilder
+ for (int i = 0; i < liveFeedBuffer.size(); i++) {
+ sb.append(liveFeedBuffer.get(i)); // Add all updates to the buffer
+ }
+ screen.getLiveFeedText().setText(sb.toString());
+ }
- onFinishScan(true, singleDirectory);
+ // Stop polling and end the process
+ scanStatusTimer.stop();
+ onFinishScan(true, singleDirectory); // Complete the scan
}
} catch (LiveFeedException | ExecutionException | InterruptedException | JsonProcessingException ex) {
- log.error(StringConstants.SCAN_LIVE_FEED_ERROR_PREFIX + " {}", ex.getMessage());
- log.debug("Debug Live Feed Exception", ex);
- SwingUtilities.invokeLater(() -> screen.showError(StringConstants.SCAN_LIVE_FEED_ERROR_PREFIX));
+ log.error("Error fetching live feed: {}", ex.getMessage());
+ log.debug("Live Feed Exception", ex);
+ SwingUtilities.invokeLater(() -> screen.showError("Live feed error"));
scanStatusTimer.stop(); // Stop polling on error
- onFinishScan(false, singleDirectory);
+ onFinishScan(false, singleDirectory); // Handling errors
}
});
scanStatusTimer.start();
diff --git a/src/main/java/org/pwss/controller/LoginController.java b/src/main/java/org/pwss/controller/LoginController.java
index 7e3a4a4..6a10f0f 100644
--- a/src/main/java/org/pwss/controller/LoginController.java
+++ b/src/main/java/org/pwss/controller/LoginController.java
@@ -2,6 +2,7 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import java.util.concurrent.ExecutionException;
+import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import org.pwss.app_settings.AppConfig;
import org.pwss.exception.user.CreateUserException;
@@ -10,10 +11,11 @@
import org.pwss.navigation.NavigationEvents;
import org.pwss.navigation.Screen;
import org.pwss.service.AuthService;
+import org.pwss.util.LoginUtil;
+import org.pwss.util.StringConstants;
import org.pwss.view.screen.LoginScreen;
import org.slf4j.LoggerFactory;
-
import static org.pwss.app_settings.AppConfig.LICENSE_KEY;
/**
@@ -58,41 +60,50 @@ public LoginController(LoginScreen view) {
@Override
public void onShow() {
- getScreen().getUsernameField().setText("");
- getScreen().getPasswordField().setText("");
+ super.onShow();
+ screen.getUsernameField().setText("");
+ screen.getPasswordField().setText("");
log.debug("Current LICENSE_KEY: {}", licenseKeySet ? "SET" : "NOT SET");
log.debug("Create User Mode: {}", createUserMode);
+ // Adjust frame size based on create user mode
+ final int frameHeight = createUserMode ? 225 : 200;
+ final int offset = licenseKeySet ? 0 : 50;
+ screen.getParentFrame().setSize(450, frameHeight + offset);
refreshView();
}
@Override
protected void initListeners() {
- getScreen().getPasswordField().addActionListener(e -> proceedAndValidate());
- getScreen().getProceedButton().addActionListener(e -> proceedAndValidate());
- getScreen().getCancelButton().addActionListener(e -> System.exit(0));
+ screen.getPasswordField().addActionListener(e -> proceedAndValidate());
+ screen.getProceedButton().addActionListener(e -> proceedAndValidate());
+ screen.getCancelButton().addActionListener(e -> System.exit(0));
}
@Override
protected void refreshView() {
// Only show license key fields if LICENSE_KEY is not set
if (licenseKeySet) {
- getScreen().getLicenseLabel().setVisible(false);
- getScreen().getLicenseKeyField().setVisible(false);
+ screen.getLicenseLabel().setVisible(false);
+ screen.getLicenseKeyField().setVisible(false);
}
// Update the view based on whether we are in create user mode
if (createUserMode) {
// Notify user that no users exist and they need to create one
- getScreen().showInfo("No user found.\nCreate one by entering a username and password.");
+ screen.showInfo("No user found.\nCreate one by entering a username and password.");
// Update message label to indicate user creation
- getScreen().setMessage("Create a user for the first login.");
+ screen.setMessage("Create a user for the first login.");
// Change button text to "Register"
- getScreen().getProceedButton().setText("Register");
+ screen.getProceedButton().setText("Register");
} else {
// Update message label to indicate normal login
- getScreen().setMessage("Login with your username and password.");
+ screen.setMessage("Login with your username and password.");
// Change button text to "Login"
- getScreen().getProceedButton().setText("Login");
+ screen.getProceedButton().setText("Login");
}
+
+ // Show or hide confirm password fields based on create user mode
+ screen.getConfirmPasswordLabel().setVisible(createUserMode);
+ screen.getConfirmPasswordField().setVisible(createUserMode);
}
/**
@@ -136,7 +147,22 @@ private void proceedAndValidate() {
if (createUserMode) {
// Handle user creation logic here and then login
- createUserAndLogin();
+ int choice = screen.showOptionDialog(JOptionPane.INFORMATION_MESSAGE,
+ """
+ Welcome to Integrity Hash!
+
+ You are about to set up access to this application. Please make sure to remember your credentials as they will be required for future logins.
+
+ Do you want to proceed?
+ """,
+ new String[] { StringConstants.GENERIC_YES, StringConstants.GENERIC_NO },
+ StringConstants.GENERIC_YES);
+
+ if (choice == 0) {
+ createUserAndLogin();
+ } else {
+ log.debug("User creation cancelled by user.");
+ }
} else {
// Handle normal login logic here
performLogin();
@@ -144,31 +170,23 @@ private void proceedAndValidate() {
}
/**
- * Validates the input fields for username and password.
+ * Validates the user input for username, password, and license key.
*
- * @return true if both fields are non-empty, false otherwise.
+ * @return true if input is valid, false otherwise.
*/
private boolean validateInput() {
- String username = getScreen().getUsername();
- String password = getScreen().getPassword();
- String licenseKey = licenseKeySet ? LICENSE_KEY : getScreen().getLicenseKey();
+ String username = screen.getUsername();
+ String password = screen.getPassword();
+ String confirmPassword = screen.getConfirmPassword();
+ String licenseKey = licenseKeySet ? LICENSE_KEY : screen.getLicenseKey();
- if (username == null || username.trim().isEmpty()) {
- getScreen().showError("Username cannot be empty.");
- return false;
- }
-
- if (password == null || password.trim().isEmpty()) {
- getScreen().showError("Password cannot be empty.");
- return false;
- }
-
- if (licenseKey == null || licenseKey.trim().isEmpty()) {
- getScreen().showError("License Key cannot be empty.");
- return false;
+ LoginUtil.LoginValidationResult result = LoginUtil.validateInput(username, password, confirmPassword,
+ licenseKey, createUserMode);
+ if (!result.isValid()) {
+ screen.showError(LoginUtil.formatErrors(result.errors()));
}
- return true;
+ return result.isValid();
}
/**
@@ -186,9 +204,9 @@ private void createUserAndLogin() {
* @return true if user creation is successful, false otherwise.
*/
private boolean createUser() {
- String username = getScreen().getUsername();
- String password = getScreen().getPassword();
- String licenseKey = licenseKeySet ? LICENSE_KEY : getScreen().getLicenseKey();
+ String username = screen.getUsername();
+ String password = screen.getPassword();
+ String licenseKey = licenseKeySet ? LICENSE_KEY : screen.getLicenseKey();
try {
final boolean createSuccess = authService.createUser(username, password, licenseKey);
@@ -224,9 +242,9 @@ private boolean createUser() {
* Displays success or error messages based on the outcome.
*/
private void performLogin() {
- String username = getScreen().getUsername();
- String password = getScreen().getPassword();
- String licenseKey = licenseKeySet ? LICENSE_KEY : getScreen().getLicenseKey();
+ String username = screen.getUsername();
+ String password = screen.getPassword();
+ String licenseKey = licenseKeySet ? LICENSE_KEY : screen.getLicenseKey();
try {
final boolean loginSuccess = authService.login(username, password, licenseKey);
@@ -235,7 +253,7 @@ private void performLogin() {
if (createUserMode) {
screen.showInfo("User created and logged in successfully!");
} else {
- screen.showInfo("Logged in successfully!");
+ log.info("Logged in successfully!");
}
AppConfig.setLicenseKey(licenseKey);
NavigationEvents.navigateTo(Screen.HOME);
diff --git a/src/main/java/org/pwss/controller/NewDirectoryController.java b/src/main/java/org/pwss/controller/NewDirectoryController.java
index ad5da69..84cb407 100644
--- a/src/main/java/org/pwss/controller/NewDirectoryController.java
+++ b/src/main/java/org/pwss/controller/NewDirectoryController.java
@@ -5,7 +5,8 @@
import org.pwss.navigation.NavigationEvents;
import org.pwss.navigation.Screen;
import org.pwss.service.MonitoredDirectoryService;
-import org.pwss.utils.StringConstants;
+import org.pwss.util.OSUtil;
+import org.pwss.util.StringConstants;
import org.pwss.view.screen.NewDirectoryScreen;
/**
@@ -35,6 +36,7 @@ public NewDirectoryController(NewDirectoryScreen screen) {
@Override
public void onShow() {
+ super.onShow();
// Reset selected path when the screen is shown
selectedPath = null;
refreshView();
@@ -42,16 +44,23 @@ public void onShow() {
@Override
void initListeners() {
- getScreen().getSelectPathButton().addActionListener(e -> openFolderPicker());
- getScreen().getCancelButton().addActionListener(e -> NavigationEvents.navigateTo(Screen.HOME));
- getScreen().getCreateButton().addActionListener(e -> createNewDirectory());
+ screen.getSelectPathButton().addActionListener(e -> openFolderPicker());
+ screen.getCancelButton().addActionListener(e -> NavigationEvents.navigateTo(Screen.HOME));
+ screen.getCreateButton().addActionListener(e -> createNewDirectory());
}
@Override
void refreshView() {
- getScreen().getPathLabel()
- .setText(selectedPath != null ? selectedPath : StringConstants.NEW_DIR_NO_PATH_SELECTED);
- getScreen().getCreateButton().setEnabled(selectedPath != null && !selectedPath.isEmpty());
+ screen.getPathLabel().setText(selectedPath != null ? selectedPath : StringConstants.NEW_DIR_NO_PATH_SELECTED);
+ screen.getCreateButton().setEnabled(selectedPath != null && !selectedPath.isEmpty());
+
+ // Additional check for Unix-based systems to disable creation for /dev and /proc paths
+ if (OSUtil.isUnix() && selectedPath != null) {
+ if (selectedPath.startsWith("/dev") || selectedPath.startsWith("/proc")) {
+ screen.getCreateButton().setEnabled(false);
+ screen.showError("Cannot monitor directories under /dev or /proc on Unix-based systems.");
+ }
+ }
}
/**
@@ -74,16 +83,16 @@ private void openFolderPicker() {
* Navigates back to the home screen upon successful creation.
*/
private void createNewDirectory() {
- boolean includeSubdirectories = getScreen().getIncludeSubdirectoriesCheckBox().isSelected();
- boolean makeActive = getScreen().getMakeDirectoryActiveCheckBox().isSelected();
+ boolean includeSubdirectories = screen.getIncludeSubdirectoriesCheckBox().isSelected();
+ boolean makeActive = screen.getMakeDirectoryActiveCheckBox().isSelected();
if (selectedPath != null && !selectedPath.isEmpty()) {
try {
monitoredDirectoryService.createNewMonitoredDirectory(selectedPath, includeSubdirectories, makeActive);
- JOptionPane.showMessageDialog(getScreen().getRootPanel(), StringConstants.NEW_DIR_SUCCESS_TEXT,
+ JOptionPane.showMessageDialog(screen.getRootPanel(), StringConstants.NEW_DIR_SUCCESS_TEXT,
StringConstants.GENERIC_SUCCESS, JOptionPane.INFORMATION_MESSAGE);
NavigationEvents.navigateTo(Screen.HOME);
} catch (Exception e) {
- JOptionPane.showMessageDialog(getScreen().getRootPanel(),
+ JOptionPane.showMessageDialog(screen.getRootPanel(),
StringConstants.NEW_DIR_ERROR_PREFIX + e.getMessage(), StringConstants.GENERIC_ERROR,
JOptionPane.ERROR_MESSAGE);
}
diff --git a/src/main/java/org/pwss/controller/ScanDetailsController.java b/src/main/java/org/pwss/controller/ScanDetailsController.java
index 1222bfd..0c810b7 100644
--- a/src/main/java/org/pwss/controller/ScanDetailsController.java
+++ b/src/main/java/org/pwss/controller/ScanDetailsController.java
@@ -15,16 +15,22 @@
import org.pwss.service.FileService;
import org.pwss.service.ScanService;
import org.pwss.service.ScanSummaryService;
-import org.pwss.utils.OSUtils;
-import org.pwss.utils.ReportUtils;
-import org.pwss.utils.StringConstants;
+import org.pwss.util.OSUtil;
+import org.pwss.util.ReportUtil;
+import org.pwss.util.StringConstants;
import org.pwss.view.screen.ScanDetailsScreen;
+import org.slf4j.LoggerFactory;
/**
* Controller class that handles operations related to the scan details screen.
*/
public class ScanDetailsController extends BaseController implements CellButtonListener {
+ /**
+ * Logger for logging messages within this controller.
+ */
+ private final org.slf4j.Logger log = LoggerFactory.getLogger(ScanDetailsController.class);
+
/**
* Service responsible for managing scan summaries.
*/
@@ -43,12 +49,18 @@ public class ScanDetailsController extends BaseController imp
* List of scan summaries. This can be empty if no summaries are available.
*/
private List scanSummaries;
+
/**
* List of differences (diffs) between scans. This can be empty if no diffs are
* available.
*/
private List diffs;
+ /**
+ * Count of differences found. This can be null if the count is not available.
+ */
+ private Integer diffCount = 0;
+
/**
* Constructs a ScanDetailsController with the given screen and initializes
* services and lists.
@@ -66,6 +78,7 @@ public ScanDetailsController(ScanDetailsScreen screen) {
@Override
public void onShow() {
+ super.onShow();
if (scanSummaries != null && !scanSummaries.isEmpty()) {
// Clear existing data
scanSummaries = List.of();
@@ -90,6 +103,8 @@ private void fetchData() {
if (scanId != null) {
// Fetch scan summaries for the scan
scanSummaries = scanSummaryService.getScanSummaryForScan(scanId);
+ // Fetch diff count for the scan
+ diffCount = scanService.getScanDiffsCount(scanId);
// Fetch diffs for the scan
diffs = scanService.getDiffs(scanId, 1000, null, true);
}
@@ -107,7 +122,7 @@ void initListeners() {
int selectedRow = screen.getScanSummaryTable().getSelectedRow();
if (selectedRow >= 0 && selectedRow < scanSummaries.size()) {
ScanSummary selectedSummary = scanSummaries.get(selectedRow);
- screen.getScanSummaryDetails().setText(ReportUtils.formatSummary(selectedSummary));
+ screen.getScanSummaryDetails().setText(ReportUtil.formatSummary(selectedSummary));
} else {
screen.getScanSummaryDetails().setText("");
screen.getDiffDetails().setText("");
@@ -118,7 +133,7 @@ void initListeners() {
int selectedRow = screen.getDiffTable().getSelectedRow();
if (selectedRow >= 0 && selectedRow < diffs.size()) {
Diff selectedDiff = diffs.get(selectedRow);
- screen.getDiffDetails().setText(ReportUtils.formatDiff(selectedDiff));
+ screen.getDiffDetails().setText(ReportUtil.formatDiff(selectedDiff));
} else {
screen.getDiffDetails().setText("");
}
@@ -134,12 +149,16 @@ void refreshView() {
SimpleSummaryTableModel simpleSummaryTableModel = new SimpleSummaryTableModel(scanSummaries);
screen.getScanSummaryTable().setModel(simpleSummaryTableModel);
+ // Update diffs count label
+ screen.getDiffsCountLabel().setText("Diffs found: " + diffCount);
+
// Populate diffs table
DiffTableModel diffTableModel = new DiffTableModel(diffs);
screen.getDiffTable().setModel(diffTableModel);
screen.getDiffTable().getColumn(DiffTableModel.columns[3]).setCellRenderer(new ButtonRenderer());
- screen.getDiffTable().getColumn(DiffTableModel.columns[3]).setCellEditor(new ButtonEditor("\uD83D\uDCE5", this));
+ screen.getDiffTable().getColumn(DiffTableModel.columns[3])
+ .setCellEditor(new ButtonEditor("\uD83D\uDCE5", this));
}
@Override
@@ -150,22 +169,29 @@ public void onCellButtonClicked(int row, int column) {
diff.ifPresent(d -> {
int choice = screen.showOptionDialog(
JOptionPane.WARNING_MESSAGE,
- OSUtils.getQuarantineWarningMessage(),
- new String[]{StringConstants.GENERIC_YES, StringConstants.GENERIC_NO},
- StringConstants.GENERIC_NO
- );
+ OSUtil.getQuarantineWarningMessage(),
+ new String[] { StringConstants.GENERIC_YES, StringConstants.GENERIC_NO },
+ StringConstants.GENERIC_NO);
if (choice == 0) {
try {
boolean success = fileService.quarantineFile(d.integrityFail().file().id());
if (success) {
- screen.showInfo("File quarantined successfully.");
+ log.info("File quarantined successfully.");
screen.getDiffTable().getColumn(d).setCellRenderer(new ButtonRenderer());
screen.getDiffTable().getColumn(d).setCellEditor(new ButtonEditor("🗿", this));
} else {
screen.showError("Failed to quarantine the file.");
}
- } catch (Exception e) {
- screen.showError(e.getMessage());
+
+ }
+
+ catch (IllegalArgumentException iae) {
+ log.debug("Illegal argument exception {}",iae);
+ }
+
+ catch (Exception e) {
+ log.debug("Exception thrown {} ", e);
+ log.error("Error code 455 : {}",e.getMessage());
}
}
});
diff --git a/src/main/java/org/pwss/data_structure/RingBuffer.java b/src/main/java/org/pwss/data_structure/RingBuffer.java
new file mode 100644
index 0000000..becde28
--- /dev/null
+++ b/src/main/java/org/pwss/data_structure/RingBuffer.java
@@ -0,0 +1,78 @@
+package org.pwss.data_structure;
+
+/**
+ * A thread-safe, fixed-size ring buffer implementation.
+ *
+ * @param The type of elements held in this collection.
+ */
+public final class RingBuffer {
+ private final Object[] buffer;
+ private int writeIndex = 0;
+ private int readIndex = 0;
+ private int size = 0;
+
+ /**
+ * Constructs a new ring buffer with the specified capacity.
+ *
+ * @param capacity The maximum number of elements that can be stored in this
+ * buffer.
+ */
+ public RingBuffer(int capacity) {
+ buffer = new Object[capacity];
+ }
+
+ /**
+ * Adds an item to the end of the buffer. If the buffer is full, this method
+ * will overwrite the oldest element with the new one.
+ *
+ * @param item The item to add to the buffer.
+ */
+ public void add(T item) {
+ buffer[writeIndex] = item;
+ writeIndex = (writeIndex + 1) % buffer.length;
+
+ if (size == buffer.length) {
+ // Buffer is full, move read pointer forward.
+ readIndex = (readIndex + 1) % buffer.length;
+ } else {
+ size++;
+ }
+ }
+
+ /**
+ * Retrieves the item at the specified index from this buffer. The first element
+ * in
+ * the buffer has an index of zero.
+ *
+ * @param index The index of the item to retrieve.
+ * @return The item at the specified position in this buffer.
+ * @throws IndexOutOfBoundsException If the specified index is greater than or
+ * equal to the size of this
+ * buffer.
+ */
+ public T get(int index) {
+ if (index >= size) {
+ throw new IndexOutOfBoundsException();
+ }
+ int realIndex = (readIndex + index) % buffer.length;
+ return (T) buffer[realIndex];
+ }
+
+ /**
+ * Returns the number of elements in this buffer.
+ *
+ * @return The current number of elements in this buffer.
+ */
+ public int size() {
+ return size;
+ }
+
+ /**
+ * Checks whether this buffer is empty or not.
+ *
+ * @return True if this buffer contains no elements; false otherwise.
+ */
+ public boolean isEmpty() {
+ return size == 0;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/pwss/exception/scan/GetDiffCountException.java b/src/main/java/org/pwss/exception/scan/GetDiffCountException.java
new file mode 100644
index 0000000..9030fd9
--- /dev/null
+++ b/src/main/java/org/pwss/exception/scan/GetDiffCountException.java
@@ -0,0 +1,25 @@
+package org.pwss.exception.scan;
+
+import java.io.Serial;
+import org.pwss.exception.PWSSbaseException;
+
+/**
+ * Exception thrown when there is an error while getting the difference count
+ * during a scan operation.
+ */
+public class GetDiffCountException extends PWSSbaseException {
+ /**
+ * Serial version UID for serialization.
+ */
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Constructs a new GetDiffCountException with the specified detail message.
+ *
+ * @param message the detail message.
+ */
+ public GetDiffCountException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/org/pwss/model/entity/MonitoredDirectory.java b/src/main/java/org/pwss/model/entity/MonitoredDirectory.java
index 6ff361e..387a5a1 100644
--- a/src/main/java/org/pwss/model/entity/MonitoredDirectory.java
+++ b/src/main/java/org/pwss/model/entity/MonitoredDirectory.java
@@ -3,6 +3,51 @@
import java.util.Date;
-public record MonitoredDirectory(long id, String path, boolean isActive, Time addedAt, Date lastScanned, Notes notes,
- boolean baselineEstablished, boolean includeSubdirectories) {
-}
+/**
+ * A record class representing a monitored directory.
+ * This class stores information about directories that are being monitored,
+ * including their ID, path, active status, timestamps, and other relevant details.
+ */
+public record MonitoredDirectory(
+ /**
+ * The unique identifier for this monitored directory.
+ */
+ long id,
+
+ /**
+ * The file system path of the monitored directory.
+ */
+ String path,
+
+ /**
+ * A flag indicating whether the directory is currently active (being monitored).
+ */
+ boolean isActive,
+
+ /**
+ * The time when this directory was added to monitoring.
+ */
+ Time addedAt,
+
+ /**
+ * The date and time when this directory was last scanned.
+ */
+ Date lastScanned,
+
+ /**
+ * Additional notes or comments about this monitored directory.
+ */
+ Notes notes,
+
+ /**
+ * A flag indicating whether a baseline has been established for this directory.
+ * This might be used to determine if the initial state of the directory has been recorded.
+ */
+ boolean baselineEstablished,
+
+ /**
+ * A flag indicating whether subdirectories should also be monitored
+ * when monitoring this directory.
+ */
+ boolean includeSubdirectories) {
+}
\ No newline at end of file
diff --git a/src/main/java/org/pwss/model/request/scan/ScanDiffsCountRequest.java b/src/main/java/org/pwss/model/request/scan/ScanDiffsCountRequest.java
new file mode 100644
index 0000000..cca6fef
--- /dev/null
+++ b/src/main/java/org/pwss/model/request/scan/ScanDiffsCountRequest.java
@@ -0,0 +1,9 @@
+package org.pwss.model.request.scan;
+
+/**
+ * Represents a request to count differences in a scan.
+ *
+ * @param scanId the identifier for the scan to count differences for
+ */
+public record ScanDiffsCountRequest(long scanId) {
+}
diff --git a/src/main/java/org/pwss/model/request/scan/StartScanAllRequest.java b/src/main/java/org/pwss/model/request/scan/StartScanAllRequest.java
new file mode 100644
index 0000000..c12e6c0
--- /dev/null
+++ b/src/main/java/org/pwss/model/request/scan/StartScanAllRequest.java
@@ -0,0 +1,9 @@
+package org.pwss.model.request.scan;
+
+/**
+ * Represents a request to start a scan for all files.
+ *
+ * @param maxHashExtractionFileSize The maximum file size (in bytes) for which hash extraction should be performed.
+ */
+public record StartScanAllRequest(long maxHashExtractionFileSize) {
+}
diff --git a/src/main/java/org/pwss/model/request/scan/StartSingleScanRequest.java b/src/main/java/org/pwss/model/request/scan/StartSingleScanRequest.java
index 00c3a53..c6c01dd 100644
--- a/src/main/java/org/pwss/model/request/scan/StartSingleScanRequest.java
+++ b/src/main/java/org/pwss/model/request/scan/StartSingleScanRequest.java
@@ -4,6 +4,7 @@
* Request to start a single scan by a monitored directory's ID.
*
* @param id The ID of the monitored directory to scan.
+ * @param maxHashExtractionFileSize The maximum file size (in bytes) for which hash extraction should be performed.
*/
-public record StartSingleScanRequest(long id) {
+public record StartSingleScanRequest(long id, long maxHashExtractionFileSize) {
}
diff --git a/src/main/java/org/pwss/model/table/MonitoredDirectoryTableModel.java b/src/main/java/org/pwss/model/table/MonitoredDirectoryTableModel.java
index cc05bd2..3bfd296 100644
--- a/src/main/java/org/pwss/model/table/MonitoredDirectoryTableModel.java
+++ b/src/main/java/org/pwss/model/table/MonitoredDirectoryTableModel.java
@@ -1,19 +1,22 @@
package org.pwss.model.table;
import java.util.Date;
+
import java.util.List;
import java.util.Optional;
import javax.swing.table.AbstractTableModel;
import org.pwss.model.entity.MonitoredDirectory;
+import org.pwss.util.MonitoredDirectoryUtil;
public class MonitoredDirectoryTableModel extends AbstractTableModel {
private final List directories;
private final String[] columnNames = {
- "\uD83D\uDCC1 Path", "\uD83D\uDEA6 Note", "\uD83D\uDD59 Last Scanned", "\uD83D\uDEE1️ Baseline Established", "\uD83D\uDDC2️ Include Subdirectories"
+ "\uD83D\uDCC1 Path", "\uD83D\uDCDD Note", "\uD83D\uDD59 Last Scanned", "\u2693 Baseline Established",
+ "\uD83D\uDCC2 Include Subdirectories", "\uD83D\uDD0C Active"
};
public MonitoredDirectoryTableModel(List directories) {
- this.directories = directories;
+ this.directories = MonitoredDirectoryUtil.filterMonitoredDirectoriesOnConfirmedPath(directories);
}
@Override
@@ -35,7 +38,7 @@ public String getColumnName(int column) {
public Class> getColumnClass(int columnIndex) {
return switch (columnIndex) {
case 2 -> Date.class;
- case 4 -> Boolean.class;
+ case 4, 5 -> Boolean.class;
default -> super.getColumnClass(columnIndex);
};
}
@@ -49,6 +52,7 @@ public Object getValueAt(int rowIndex, int columnIndex) {
case 2 -> dir.lastScanned();
case 3 -> dir.baselineEstablished() ? "Yes" : "No";
case 4 -> dir.includeSubdirectories();
+ case 5 -> dir.isActive();
default -> null;
};
}
@@ -57,8 +61,9 @@ public Object getValueAt(int rowIndex, int columnIndex) {
* Get the MonitoredDirectory object at the specified row index.
*
* @param rowIndex the index of the row in the table.
- * @return an Optional containing the MonitoredDirectory object at the specified row index,
- * or an empty Optional if the index is out of bounds.
+ * @return an Optional containing the MonitoredDirectory object at the specified
+ * row index,
+ * or an empty Optional if the index is out of bounds.
*/
public Optional getDirectoryAt(int rowIndex) {
if (rowIndex >= 0 && rowIndex < directories.size()) {
@@ -67,4 +72,3 @@ public Optional getDirectoryAt(int rowIndex) {
return Optional.empty();
}
}
-
diff --git a/src/main/java/org/pwss/model/table/QuarantineTableModel.java b/src/main/java/org/pwss/model/table/QuarantineTableModel.java
index 0262e4f..05cd750 100644
--- a/src/main/java/org/pwss/model/table/QuarantineTableModel.java
+++ b/src/main/java/org/pwss/model/table/QuarantineTableModel.java
@@ -4,7 +4,7 @@
import java.util.Optional;
import javax.swing.table.AbstractTableModel;
import org.pwss.model.entity.QuarantineMetadata;
-import org.pwss.utils.OSUtils;
+import org.pwss.util.OSUtil;
/**
* Table model for displaying quarantine metadata in a table.
@@ -41,7 +41,7 @@ public Object getValueAt(int rowIndex, int columnIndex) {
QuarantineMetadata metadata = data.get(rowIndex);
return switch (columnIndex) {
case 0 -> metadata.fileId();
- case 1 -> OSUtils.formatQuarantinePath(metadata.keyName());
+ case 1 -> OSUtil.formatQuarantinePath(metadata.keyName());
case 2 -> "\uD83D\uDCE4";
default -> null;
};
diff --git a/src/main/java/org/pwss/model/table/ScanTableModel.java b/src/main/java/org/pwss/model/table/ScanTableModel.java
index a9ae6b4..6603d17 100644
--- a/src/main/java/org/pwss/model/table/ScanTableModel.java
+++ b/src/main/java/org/pwss/model/table/ScanTableModel.java
@@ -8,7 +8,7 @@
public class ScanTableModel extends AbstractTableModel {
private final List scans;
private final String[] columnNames = {
- "\uD83D\uDCC1 Directory", "\uD83D\uDD59 Scan Time", "\uD83D\uDEA6 Status"
+ "\uD83D\uDCC1 Directory", "\uD83D\uDD59 Scan Time", "⌛ Status"
};
public ScanTableModel(List scans) {
diff --git a/src/main/java/org/pwss/navigation/NavigationHandler.java b/src/main/java/org/pwss/navigation/NavigationHandler.java
index 36530a7..661ef7c 100644
--- a/src/main/java/org/pwss/navigation/NavigationHandler.java
+++ b/src/main/java/org/pwss/navigation/NavigationHandler.java
@@ -50,6 +50,7 @@ public void navigateTo(Screen screen, NavigationContext context) {
// Set the navigation context for the controller
controller.setContext(context);
BaseScreen baseScreen = controller.getScreen();
+ baseScreen.setParentFrame(frame);
// Ensure the controller reloads its data when navigating to the screen
controller.reloadData();
diff --git a/src/main/java/org/pwss/navigation/Screen.java b/src/main/java/org/pwss/navigation/Screen.java
index 36092fc..bb9c5b9 100644
--- a/src/main/java/org/pwss/navigation/Screen.java
+++ b/src/main/java/org/pwss/navigation/Screen.java
@@ -7,7 +7,7 @@ public enum Screen {
/**
* The login screen where users can authenticate.
*/
- LOGIN(400, 200),
+ LOGIN(450, 250),
/**
* The home screen displayed after successful login.
diff --git a/src/main/java/org/pwss/service/FileService.java b/src/main/java/org/pwss/service/FileService.java
index 5c5be7b..d1a4314 100644
--- a/src/main/java/org/pwss/service/FileService.java
+++ b/src/main/java/org/pwss/service/FileService.java
@@ -57,7 +57,7 @@ public FileService() {
* @return true if the file was successfully quarantined; false otherwise.
* @throws QuarantineFileException If the file cannot be quarantined due to
* various reasons such as
- * unauthorized access, unprocessable entity, or
+ * unauthorized access, bad request, or
* server errors.
* @throws JsonProcessingException If there is an error during JSON
* serialization or deserialization.
@@ -84,8 +84,8 @@ public boolean quarantineFile(long fileId) throws QuarantineFileException, JsonP
return switch (response.statusCode()) {
case 200 -> parsed.map(QuarantineResponse::successful).orElse(false);
+ case 400 -> throw new QuarantineFileException("File cannot be quarantined: " + fileId);
case 401 -> throw new QuarantineFileException("Unauthorized: Invalid credentials for file quarantine");
- case 422 -> throw new QuarantineFileException("File cannot be quarantined: " + fileId);
case 500 -> throw new QuarantineFileException("Server error during file quarantine");
default -> throw new QuarantineFileException("File quarantine failed: Unexpected status code ");
};
@@ -99,7 +99,7 @@ public boolean quarantineFile(long fileId) throws QuarantineFileException, JsonP
* @return A `QuarantineResponse` object containing the response details.
* @throws UnquarantineFileException If the file cannot be unquarantined due to
* various reasons such as
- * unauthorized access, unprocessable entity,
+ * unauthorized access, bad request,
* or server errors.
* @throws JsonProcessingException If there is an error during JSON
* serialization or deserialization.
@@ -125,8 +125,8 @@ public boolean unquarantineFile(QuarantineMetadata metadata) throws Unquarantine
return switch (response.statusCode()) {
case 200 -> true;
+ case 400 -> throw new UnquarantineFileException("File cannot be unquarantined: " + metadata.keyName());
case 401 -> throw new UnquarantineFileException("Unauthorized: Invalid credentials for file unquarantine");
- case 422 -> throw new UnquarantineFileException("File cannot be unquarantined: " + metadata.keyName());
case 500 -> throw new UnquarantineFileException("Server error during file unquarantine");
default -> throw new UnquarantineFileException("File unquarantine failed: Unexpected status code ");
};
diff --git a/src/main/java/org/pwss/service/MonitoredDirectoryService.java b/src/main/java/org/pwss/service/MonitoredDirectoryService.java
index 166c733..8e3b9bb 100644
--- a/src/main/java/org/pwss/service/MonitoredDirectoryService.java
+++ b/src/main/java/org/pwss/service/MonitoredDirectoryService.java
@@ -153,48 +153,77 @@ public MonitoredDirectory createNewMonitoredDirectory(String path, boolean inclu
}
/**
- * Updates an existing monitored directory by sending a request to the
- * MONITORED_DIRECTORY_UPDATE endpoint.
+ * Toggles the active status of a monitored directory by sending a request to
+ * the MONITORED_DIRECTORY_UPDATE endpoint.
*
- * @param id The ID of the monitored directory to update.
- * @param isActive The new active status of the monitored directory.
- * @param notes Any notes associated with the monitored directory.
- * @param includeSubDirs Whether to include subdirectories in monitoring.
+ * @param dir The MonitoredDirectory object representing the directory to be
+ * updated.
* @return `true` if the update was successful, otherwise false.
* @throws UpdateMonitoredDirectoryException If the update attempt fails due to
- * invalid input, unauthorized access,
- * or server error.
+ * invalid input, unauthorized access,
+ * or server error.
* @throws ExecutionException If an error occurs during the
- * asynchronous execution of the
- * request.
+ * asynchronous execution of the request.
* @throws InterruptedException If the thread executing the request
- * is interrupted.
+ * is interrupted.
* @throws JsonProcessingException If an error occurs while
- * serializing the request body.
+ * serializing the request body.
*/
- public boolean updateMonitoredDirectory(long id, boolean isActive, String notes, boolean includeSubDirs)
- throws UpdateMonitoredDirectoryException, JsonProcessingException, ExecutionException,
- InterruptedException {
+ public boolean toggleActive(MonitoredDirectory dir) throws UpdateMonitoredDirectoryException, JsonProcessingException, ExecutionException, InterruptedException {
String body = objectMapper
- .writeValueAsString(new UpdateDirectoryRequest(id, isActive, notes, includeSubDirs));
+ .writeValueAsString(new UpdateDirectoryRequest(dir.id(), !dir.isActive(), dir.notes().notes(), dir.includeSubdirectories()));
HttpResponse response = PwssHttpClient.getInstance()
.request(Endpoint.MONITORED_DIRECTORY_UPDATE, body);
return switch (response.statusCode()) {
case 200 -> true;
+ case 400 ->
+ throw new UpdateMonitoredDirectoryException("Update monitored directory failed: invalid input data.");
+ case 401 ->
+ throw new UpdateMonitoredDirectoryException("Update monitored directory failed: User not authorized to perform this action.");
+ case 500 ->
+ throw new UpdateMonitoredDirectoryException("Update monitored directory failed: An error occurred on the server while attempting to update the monitored directory.");
+ default -> false;
+ };
+ }
+
+ /**
+ * Toggles the inclusion of subdirectories for a monitored directory by sending
+ * a request to the MONITORED_DIRECTORY_UPDATE endpoint.
+ *
+ * @param dir The MonitoredDirectory object representing the directory to be
+ * updated.
+ * @return `true` if the update was successful, otherwise false.
+ * @throws UpdateMonitoredDirectoryException If the update attempt fails due to
+ * invalid input, unauthorized access,
+ * or server error.
+ * @throws ExecutionException If an error occurs during the
+ * asynchronous execution of the request.
+ * @throws InterruptedException If the thread executing the request
+ * is interrupted.
+ * @throws JsonProcessingException If an error occurs while
+ * serializing the request body.
+ */
+ public boolean toggleIncludeSubDirectories(MonitoredDirectory dir) throws UpdateMonitoredDirectoryException, JsonProcessingException, ExecutionException, InterruptedException {
+ String body = objectMapper
+ .writeValueAsString(new UpdateDirectoryRequest(dir.id(), dir.isActive(), dir.notes().notes(), !dir.includeSubdirectories()));
+ HttpResponse response = PwssHttpClient.getInstance()
+ .request(Endpoint.MONITORED_DIRECTORY_UPDATE, body);
+
+ return switch (response.statusCode()) {
+ case 200 -> true;
+ case 400 ->
+ throw new UpdateMonitoredDirectoryException("Update monitored directory failed: invalid input data.");
case 401 ->
- throw new UpdateMonitoredDirectoryException(
- "Update monitored directory failed: User not authorized to perform this action.");
- case 422 ->
- throw new UpdateMonitoredDirectoryException(
- "Update monitored directory failed: Unprocessable entity - invalid input data.");
+ throw new UpdateMonitoredDirectoryException("Update monitored directory failed: User not authorized to perform this action.");
case 500 ->
- throw new UpdateMonitoredDirectoryException(
- "Update monitored directory failed: An error occurred on the server while attempting to update the monitored directory.");
+ throw new UpdateMonitoredDirectoryException("Update monitored directory failed: An error occurred on the server while attempting to update the monitored directory.");
default -> false;
};
}
+
+
/**
* Creates a new baseline for a monitored directory by sending a request to the
* MONITORED_DIRECTORY_NEW_BASELINE endpoint.
diff --git a/src/main/java/org/pwss/service/NoteService.java b/src/main/java/org/pwss/service/NoteService.java
index 1018a9e..07a4e1d 100644
--- a/src/main/java/org/pwss/service/NoteService.java
+++ b/src/main/java/org/pwss/service/NoteService.java
@@ -38,8 +38,8 @@ public boolean updateNotes(long noteId, String newNotes) throws UpdateNoteExcept
HttpResponse response = PwssHttpClient.getInstance().request(Endpoint.NOTE_UPDATE, body);
return switch (response.statusCode()) {
case 200 -> true;
+ case 400 -> throw new UpdateNoteException("Update notes failed: Invalid note ID or note content.");
case 401 -> throw new UpdateNoteException("Update notes failed: User not authorized to perform this action.");
- case 422 -> throw new UpdateNoteException("Update notes failed: Invalid note ID or note content.");
case 500 -> throw new UpdateNoteException("Update notes failed: An error occurred on the server while attempting to update the notes.");
default -> false;
};
@@ -61,9 +61,9 @@ public boolean restoreNotes(long noteId, RestoreNoteType restoreNoteType) throws
HttpResponse response = PwssHttpClient.getInstance().request(Endpoint.NOTE_RESTORE, body);
return switch (response.statusCode()) {
case 200 -> true;
+ case 400 -> throw new RestoreNoteException("Restore notes failed: Invalid note ID or note content.");
case 401 ->
throw new RestoreNoteException("Restore notes failed: User not authorized to perform this action.");
- case 422 -> throw new RestoreNoteException("Restore notes failed: Invalid note ID or note content.");
case 500 ->
throw new RestoreNoteException("Restore notes failed: An error occurred on the server while attempting to restore the notes.");
default -> false;
diff --git a/src/main/java/org/pwss/service/ScanService.java b/src/main/java/org/pwss/service/ScanService.java
index c932563..6f4d6b8 100644
--- a/src/main/java/org/pwss/service/ScanService.java
+++ b/src/main/java/org/pwss/service/ScanService.java
@@ -7,6 +7,7 @@
import java.util.List;
import java.util.concurrent.ExecutionException;
import org.pwss.exception.scan.GetAllMostRecentScansException;
+import org.pwss.exception.scan.GetDiffCountException;
import org.pwss.exception.scan.GetMostRecentScansException;
import org.pwss.exception.scan.GetScanDiffsException;
import org.pwss.exception.scan.LiveFeedException;
@@ -18,6 +19,8 @@
import org.pwss.model.entity.Scan;
import org.pwss.model.request.scan.GetMostRecentScansRequest;
import org.pwss.model.request.scan.GetScanDiffsRequest;
+import org.pwss.model.request.scan.ScanDiffsCountRequest;
+import org.pwss.model.request.scan.StartScanAllRequest;
import org.pwss.model.request.scan.StartSingleScanRequest;
import org.pwss.model.response.LiveFeedResponse;
import org.pwss.service.network.Endpoint;
@@ -39,23 +42,25 @@ public ScanService() {
/**
* Starts a scan by sending a request to the START_SCAN endpoint.
*
+ * @param maxHashExtractionFileSize The maximum file size for hash extraction.
* @return `true` if the scan start request is successful, otherwise `false`.
* @throws StartFullScanException If the scan start attempt fails due to various reasons such as invalid credentials, no active monitored directories, scan already running, or server error.
* @throws ExecutionException If an error occurs during the asynchronous execution of the request.
* @throws InterruptedException If the thread executing the request is interrupted.
*/
- public boolean startScan() throws StartFullScanException, ExecutionException, InterruptedException {
- HttpResponse response = PwssHttpClient.getInstance().request(Endpoint.START_SCAN, null);
+ public boolean startScan(long maxHashExtractionFileSize) throws StartFullScanException, ExecutionException, InterruptedException, JsonProcessingException {
+ String body = objectMapper.writeValueAsString(new StartScanAllRequest(maxHashExtractionFileSize));
+ HttpResponse response = PwssHttpClient.getInstance().request(Endpoint.START_SCAN, body);
return switch (response.statusCode()) {
case 200 -> true;
case 401 ->
- throw new StartFullScanException("Start scan all failed: User not authorized to perform this action.");
+ throw new StartFullScanException("User not authorized to perform this action.");
case 412 ->
- throw new StartFullScanException("Start scan all failed: No active monitored directories found.");
- case 425 -> throw new StartFullScanException("Start scan all failed: Scan is already running.");
+ throw new StartFullScanException("There are no directories being actively monitored.");
+ case 425 -> throw new StartFullScanException("Scan is already running.");
case 500 ->
- throw new StartFullScanException("Start scan all failed: An error occurred on the server while attempting to start the scan.");
+ throw new StartFullScanException("An error occurred on the server while attempting to start the scan.");
default -> false;
};
}
@@ -64,14 +69,15 @@ public boolean startScan() throws StartFullScanException, ExecutionException, In
* Starts a scan for a specific monitored directory by its ID by sending a request to the START_SCAN_ID endpoint.
*
* @param id The ID of the monitored directory to start the scan for.
+ * @param maxHashExtractionFileSize The maximum file size for hash extraction.
* @return `true` if the scan start request is successful, otherwise `false`.
* @throws StartScanByIdException If the scan start attempt fails due to various reasons such as invalid credentials, monitored directory not found, monitored directory inactive, scan already running, or server error.
* @throws ExecutionException If an error occurs during the asynchronous execution of the request.
* @throws InterruptedException If the thread executing the request is interrupted.
* @throws JsonProcessingException If an error occurs while serializing the start scan request to JSON.
*/
- public boolean startScanById(long id) throws StartScanByIdException, ExecutionException, InterruptedException, JsonProcessingException {
- String body = objectMapper.writeValueAsString(new StartSingleScanRequest(id));
+ public boolean startScanById(long id, long maxHashExtractionFileSize) throws StartScanByIdException, ExecutionException, InterruptedException, JsonProcessingException {
+ String body = objectMapper.writeValueAsString(new StartSingleScanRequest(id, maxHashExtractionFileSize));
HttpResponse response = PwssHttpClient.getInstance().request(Endpoint.START_SCAN_ID, body);
return switch (response.statusCode()) {
@@ -196,6 +202,19 @@ public List getMostRecentScansAll() throws GetAllMostRecentScansException,
};
}
+ /**
+ * Retrieves the diffs for a specific scan by sending a request to the SCAN_DIFFS endpoint.
+ *
+ * @param scanId The ID of the scan to retrieve diffs for.
+ * @param limit The maximum number of diffs to retrieve.
+ * @param sortField The field by which to sort the diffs.
+ * @param ascending Whether to sort the diffs in ascending order.
+ * @return A list of Diff objects if the request is successful.
+ * @throws GetScanDiffsException If the attempt to retrieve the scan diffs fails due to various reasons such as invalid credentials or server error.
+ * @throws ExecutionException If an error occurs during the asynchronous execution of the request.
+ * @throws InterruptedException If the thread executing the request is interrupted.
+ * @throws JsonProcessingException If an error occurs while processing JSON data.
+ */
public List getDiffs(long scanId, long limit, String sortField, boolean ascending) throws GetScanDiffsException, ExecutionException, InterruptedException, JsonProcessingException {
String body = objectMapper.writeValueAsString(new GetScanDiffsRequest(scanId, limit, sortField, ascending));
HttpResponse response = PwssHttpClient.getInstance().request(Endpoint.SCAN_DIFFS, body);
@@ -204,8 +223,32 @@ public List getDiffs(long scanId, long limit, String sortField, boolean as
case 200 -> List.of(objectMapper.readValue(response.body(), Diff[].class));
case 401 ->
throw new GetScanDiffsException("Failed to fetch scan diffs: User not authorized to perform this action.");
- case 500 -> throw new GetScanDiffsException("Failed to fetch scan diffs: Server error");
+ case 500 -> throw new GetScanDiffsException("Failed to fetch scan diffs");
default -> Collections.emptyList();
};
}
+
+ /**
+ * Retrieves the count of diffs for a specific scan by sending a request to the DIFF_COUNT endpoint.
+ *
+ * @param scanId The ID of the scan to retrieve the diff count for.
+ * @return The count of diffs for the specified scan if the request is successful.
+ * @throws GetDiffCountException If the attempt to retrieve the scan diffs count fails due to various reasons such as invalid credentials, scan not found, or server error.
+ * @throws ExecutionException If an error occurs during the asynchronous execution of the request.
+ * @throws InterruptedException If the thread executing the request is interrupted.
+ * @throws JsonProcessingException If an error occurs while processing JSON data.
+ */
+ public Integer getScanDiffsCount(long scanId) throws JsonProcessingException, ExecutionException, InterruptedException, GetDiffCountException {
+ String body = objectMapper.writeValueAsString(new ScanDiffsCountRequest(scanId));
+ HttpResponse response = PwssHttpClient.getInstance().request(Endpoint.DIFF_COUNT, body);
+
+ return switch (response.statusCode()) {
+ case 200 -> Integer.parseInt(response.body());
+ case 400 -> throw new GetDiffCountException("Get scan diffs count failed: Bad request.");
+ case 401 -> throw new GetDiffCountException("Get scan diffs count failed: User not authorized to perform this action.");
+ case 404 -> throw new GetDiffCountException("Get scan diffs count failed: Scan with the given ID not found.");
+ case 500 -> throw new GetDiffCountException("Get scan diffs count failed: An error occurred on the server while attempting to get the diff count.");
+ default -> null;
+ };
+ }
}
diff --git a/src/main/java/org/pwss/service/ScanSummaryService.java b/src/main/java/org/pwss/service/ScanSummaryService.java
index bd883a7..ac99b5e 100644
--- a/src/main/java/org/pwss/service/ScanSummaryService.java
+++ b/src/main/java/org/pwss/service/ScanSummaryService.java
@@ -5,8 +5,8 @@
import java.net.http.HttpResponse;
import java.util.List;
import java.util.concurrent.ExecutionException;
-import org.pwss.exception.scan_summary.GetMostRecentSummaryException;
import org.pwss.exception.scan_summary.FileSearchException;
+import org.pwss.exception.scan_summary.GetMostRecentSummaryException;
import org.pwss.exception.scan_summary.GetSummaryForFileException;
import org.pwss.exception.scan_summary.GetSummaryForScanException;
import org.pwss.model.entity.File;
@@ -67,12 +67,12 @@ public List getSummaryForFile(long fileId) throws GetSummaryForFile
return switch (response.statusCode()) {
case 200 -> List.of(objectMapper.readValue(response.body(), ScanSummary[].class));
+ case 400 ->
+ throw new GetSummaryForFileException("Get summaries for file failed: The provided file ID is invalid.");
case 401 ->
throw new GetSummaryForFileException("Get summaries for file failed: User not authorized to perform this action.");
case 404 ->
throw new GetSummaryForFileException("Get summaries for file failed: No scan summaries found for the specified file.");
- case 422 ->
- throw new GetSummaryForFileException("Get summaries for file failed: The provided file ID is invalid.");
case 500 ->
throw new GetSummaryForFileException("Get summaries for file failed: An error occurred on the server while attempting to retrieve the scan summaries.");
default -> null;
@@ -96,11 +96,11 @@ public List searchFiles(String queryString, boolean ascending) throws File
return switch (response.statusCode()) {
case 200 -> List.of(objectMapper.readValue(response.body(), File[].class));
+ case 400 ->
+ throw new FileSearchException("Search files failed: The provided search parameters are invalid.");
case 401 ->
throw new FileSearchException("Search files failed: User not authorized to perform this action.");
case 404 -> List.of();
- case 422 ->
- throw new FileSearchException("Search files failed: The provided search parameters are invalid.");
case 500 ->
throw new FileSearchException("Search files failed: An error occurred on the server while attempting to search for files.");
default -> null;
@@ -113,12 +113,12 @@ public List getScanSummaryForScan(long scanId) throws GetSummaryFor
return switch (response.statusCode()) {
case 200 -> List.of(objectMapper.readValue(response.body(), ScanSummary[].class));
+ case 400 ->
+ throw new GetSummaryForScanException("Get summaries for scan failed: The provided scan ID is invalid.");
case 401 ->
throw new GetSummaryForScanException("Get summaries for scan failed: User not authorized to perform this action.");
case 404 ->
throw new GetSummaryForScanException("Get summaries for scan failed: No scan summaries found for the specified file.");
- case 422 ->
- throw new GetSummaryForScanException("Get summaries for scan failed: The provided scan ID is invalid.");
case 500 ->
throw new GetSummaryForScanException("Get summaries for scan failed: An error occurred on the server while attempting to retrieve the scan summaries.");
default -> null;
diff --git a/src/main/java/org/pwss/service/network/Endpoint.java b/src/main/java/org/pwss/service/network/Endpoint.java
index c6b6ef3..a03c96a 100644
--- a/src/main/java/org/pwss/service/network/Endpoint.java
+++ b/src/main/java/org/pwss/service/network/Endpoint.java
@@ -25,7 +25,7 @@ public enum Endpoint {
/**
* Endpoint for starting a file integrity scan of all directories.
*/
- START_SCAN(HTTP_Method.GET, A.BASE_URL + A.SCAN + "start/all"),
+ START_SCAN(HTTP_Method.POST, A.BASE_URL + A.SCAN + "start/all"),
/**
* Endpoint for starting a file integrity scan of a specific directory.
*/
@@ -54,6 +54,10 @@ public enum Endpoint {
* Endpoint for retrieving diffs for a given scan.
*/
SCAN_DIFFS(HTTP_Method.POST, A.BASE_URL + A.SCAN + "diff"),
+ /**
+ * Endpoint for retrieving the count of diffs for a given scan.
+ */
+ DIFF_COUNT(HTTP_Method.POST, A.BASE_URL + A.SCAN + "diff/count"),
// Directory Endpoints
diff --git a/src/main/java/org/pwss/utils/AppTheme.java b/src/main/java/org/pwss/util/AppTheme.java
similarity index 95%
rename from src/main/java/org/pwss/utils/AppTheme.java
rename to src/main/java/org/pwss/util/AppTheme.java
index 0b86b3b..09b42ef 100644
--- a/src/main/java/org/pwss/utils/AppTheme.java
+++ b/src/main/java/org/pwss/util/AppTheme.java
@@ -1,4 +1,4 @@
-package org.pwss.utils;
+package org.pwss.util;
/**
* Enum representing different application themes.
diff --git a/src/main/java/org/pwss/util/ConversionUtil.java b/src/main/java/org/pwss/util/ConversionUtil.java
new file mode 100644
index 0000000..1399aab
--- /dev/null
+++ b/src/main/java/org/pwss/util/ConversionUtil.java
@@ -0,0 +1,100 @@
+package org.pwss.util;
+
+/**
+ * Utility class providing methods to convert between different units of data
+ * measurement, such as bytes,
+ * megabytes, and gigabytes. This class includes static utility methods that
+ * perform these conversions.
+ *
+ *
+ * The primary purpose of this class is to facilitate unit conversions in
+ * scenarios where file sizes or
+ * other data measurements need to be handled in a specific unit for easier
+ * comprehension or processing.
+ *
+ */
+public class ConversionUtil {
+ private static final long MEGABYTE = 1024L * 1024L;
+ private static final long GIGABYTE = MEGABYTE * 1024L;
+
+ /**
+ * Converts a value in bytes to megabytes.
+ *
+ * @param bytes the number of bytes to convert
+ * @return the equivalent value in megabytes as a Long
+ */
+ public static Long bytesToMegabytes(Long bytes) {
+ if (bytes == null) {
+ throw new IllegalArgumentException("Bytes value cannot be null.");
+ }
+ return bytes / MEGABYTE;
+ }
+
+ /**
+ * Converts a value in megabytes to bytes.
+ *
+ * @param megabytes the number of megabytes to convert
+ * @return the equivalent value in bytes as a Long
+ */
+ public static Long megabytesToBytes(Long megabytes) {
+ if (megabytes == null) {
+ throw new IllegalArgumentException("Megabytes value cannot be null.");
+ }
+ return megabytes * MEGABYTE;
+ }
+
+ // Optional: If you want to handle conversion with double values for more
+ // precise calculations
+ /**
+ * Converts a value in bytes to megabytes.
+ *
+ * @param bytes the number of bytes to convert
+ * @return the equivalent value in megabytes as a Double
+ */
+ public static Double bytesToMegabytesDouble(Long bytes) {
+ if (bytes == null) {
+ throw new IllegalArgumentException("Bytes value cannot be null.");
+ }
+ return (double) bytes / MEGABYTE;
+ }
+
+ /**
+ * Converts a value in megabytes to bytes.
+ *
+ * @param megabytes the number of megabytes to convert
+ * @return the equivalent value in bytes as a Double
+ */
+ public static Long megabytesToBytesDouble(Double megabytes) {
+ if (megabytes == null) {
+ throw new IllegalArgumentException("Megabytes value cannot be null.");
+ }
+ return Math.round(megabytes * MEGABYTE);
+ }
+
+ /**
+ * Converts a value in bytes to gigabytes.
+ *
+ * @param bytes the number of bytes to convert
+ * @return the equivalent value in gigabytes as a Double
+ */
+ public static Double bytesToGigabytesDouble(Long bytes) {
+ if (bytes == null) {
+ throw new IllegalArgumentException("Bytes value cannot be null.");
+ }
+ return (double) bytes / GIGABYTE;
+ }
+
+ /**
+ * Converts a value in gigabytes to bytes.
+ *
+ * @param gigabytes the number of gigabytes to convert
+ * @return the equivalent value in bytes as a Double
+ */
+ public static Long gigabytesToBytesDouble(Double gigabytes) {
+ if (gigabytes == null) {
+ throw new IllegalArgumentException("Gigabytes value cannot be null.");
+ }
+ return Math.round(gigabytes * GIGABYTE);
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/pwss/util/ErrorUtil.java b/src/main/java/org/pwss/util/ErrorUtil.java
new file mode 100644
index 0000000..5338971
--- /dev/null
+++ b/src/main/java/org/pwss/util/ErrorUtil.java
@@ -0,0 +1,21 @@
+package org.pwss.util;
+
+/**
+ * Utility class for handling and formatting error messages.
+ */
+public final class ErrorUtil {
+
+ /**
+ * Formats an error message by combining a constant text with a dynamic error
+ * message.
+ *
+ * @param errorConstantText The constant part of the error message, such as
+ * "Error: "
+ * @param errorMessageText The dynamic part of the error message that provides
+ * specific details
+ * @return A formatted error message string combining both parts
+ */
+ public static String formatErrorMessage(String errorConstantText, String errorMessageText) {
+ return errorConstantText + errorMessageText;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/pwss/util/LiveFeedUtil.java b/src/main/java/org/pwss/util/LiveFeedUtil.java
new file mode 100644
index 0000000..15ee9fc
--- /dev/null
+++ b/src/main/java/org/pwss/util/LiveFeedUtil.java
@@ -0,0 +1,70 @@
+package org.pwss.util;
+
+/**
+ * Utility class for processing live feed entries.
+ */
+public final class LiveFeedUtil {
+
+ /**
+ * A white check mark emoji used in live feed entries.
+ */
+ private final static String WHITE_CHECK_MARK = "✅";
+ /**
+ * The white check mark emoji with a newline character appended to it.
+ */
+ private final static String WHITE_CHECK_MARK_REPLACE = "✅\n";
+
+ /**
+ * A warning emoji used in live feed entries.
+ */
+ private final static String WARNING = "⚠️";
+ /**
+ * The warning emoji with a newline character appended to it.
+ */
+ private final static String WARNING_REPLACE = "⚠️\n";
+
+ /**
+ * A message indicating that a file is too big, used in live feed entries.
+ */
+ private final static String FILE_TO_BIG_MESSAGE = "is bigger than the user defined max limit";
+ /**
+ * The file too big message with a newline character appended to it.
+ */
+ private final static String FILE_TO_BIG_MESSAGE_REPLACE = FILE_TO_BIG_MESSAGE + "\n";
+
+ /**
+ * Formats a raw live feed entry for improved readability.
+ * Currently, inserts line breaks after certain emojis.
+ *
+ * @param rawEntry the raw live feed text from the service
+ * @return formatted live feed text
+ */
+ public static String formatLiveFeedEntry(String rawEntry) {
+ if (rawEntry == null || rawEntry.isEmpty()) {
+ return "";
+ }
+
+ return rawEntry
+ .replace(WHITE_CHECK_MARK, WHITE_CHECK_MARK_REPLACE)
+ .replace(WARNING, WARNING_REPLACE)
+ .replace(FILE_TO_BIG_MESSAGE, FILE_TO_BIG_MESSAGE_REPLACE)
+ .trim();
+ }
+
+ /**
+ * Counts the number of warnings in a live feed entry.
+ * Warnings are indicated by the "⚠" symbol.
+ *
+ * @param entry the live feed entry text
+ * @return the count of warnings in the entry
+ */
+ public static int countWarnings(String entry) {
+ if (entry == null || entry.isEmpty()) {
+ return 0;
+ }
+
+ return (int) entry.codePoints()
+ .filter(c -> c == 0x26A0) // ⚠
+ .count();
+ }
+}
diff --git a/src/main/java/org/pwss/util/LoginUtil.java b/src/main/java/org/pwss/util/LoginUtil.java
new file mode 100644
index 0000000..4cd2d01
--- /dev/null
+++ b/src/main/java/org/pwss/util/LoginUtil.java
@@ -0,0 +1,113 @@
+package org.pwss.util;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utility class for validating login inputs.
+ */
+public final class LoginUtil {
+
+ // Private constructor to prevent instantiation
+ private LoginUtil() {
+ // This constructor is intentionally empty.
+ }
+
+ /**
+ * Validates the login input fields.
+ *
+ * @param username The username input.
+ * @param password The password input.
+ * @param confirmPassword The confirm password input (used in create user mode).
+ * @param licenseKey The license key input.
+ * @param createUserMode Flag indicating if the application is in create user mode.
+ * @return A LoginValidationResult containing validation status and error messages.
+ */
+ public static LoginValidationResult validateInput(
+ String username,
+ String password,
+ String confirmPassword,
+ String licenseKey,
+ boolean createUserMode
+ ) {
+ List errors = new ArrayList<>();
+
+ username = username == null ? "" : username.trim();
+ password = password == null ? "" : password.trim();
+ confirmPassword = confirmPassword == null ? "" : confirmPassword.trim();
+ licenseKey = licenseKey == null ? "" : licenseKey.trim();
+
+ if (username.isEmpty()) errors.add("Username cannot be empty.");
+ if (password.isEmpty()) errors.add("Password cannot be empty.");
+ if (licenseKey.isEmpty()) errors.add("License key cannot be empty.");
+
+ if (createUserMode && !password.isEmpty()) {
+ if (password.length() < 8)
+ errors.add("Password must be at least 8 characters long.");
+
+ if (confirmPassword.isEmpty())
+ errors.add("Confirm Password cannot be empty.");
+ else if (!xorEquals(password, confirmPassword))
+ errors.add("Password and Confirm Password do not match.");
+
+ if (!password.matches(".*[A-Z].*"))
+ errors.add("Password must contain at least one uppercase letter.");
+ if (!password.matches(".*\\d.*"))
+ errors.add("Password must contain at least one digit.");
+ if (!password.matches(".*[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?].*"))
+ errors.add("Password must contain at least one special character.");
+ }
+
+ return new LoginValidationResult(errors.isEmpty(), errors);
+ }
+
+ /**
+ * Formats a list of error messages into a single string.
+ *
+ * @param errors List of error messages.
+ * @return Formatted error string.
+ */
+ public static String formatErrors(List errors) {
+ return String.join("\n", errors);
+ }
+
+
+ /**
+ * Compares two strings for equality using XOR operation.
+ *
+ * @param s1 First string.
+ * @param s2 Second string.
+ * @return True if both strings are equal, false otherwise.
+ * @throws IllegalArgumentException if either string is null.
+ */
+ private static boolean xorEquals(String s1, String s2) {
+ if (s1 == null || s2 == null) {
+ throw new IllegalArgumentException("Strings to compare cannot be null");
+ }
+
+ byte[] a = s1.getBytes(StandardCharsets.UTF_8);
+ byte[] b = s2.getBytes(StandardCharsets.UTF_8);
+
+ int maxLen = Math.max(a.length, b.length);
+ int result = a.length ^ b.length;
+
+ for (int i = 0; i < maxLen; i++) {
+ byte ba = (i < a.length) ? a[i] : 0;
+ byte bb = (i < b.length) ? b[i] : 0;
+ result |= (ba ^ bb);
+ }
+
+ return result == 0;
+ }
+
+ /**
+ * Result of login validation.
+ *
+ * @param isValid True if the input is valid, false otherwise.
+ * @param errors List of error messages if the input is invalid.
+ */
+ public record LoginValidationResult(boolean isValid, List errors) {
+ }
+}
+
diff --git a/src/main/java/org/pwss/utils/MonitoredDirectoryUtils.java b/src/main/java/org/pwss/util/MonitoredDirectoryUtil.java
similarity index 78%
rename from src/main/java/org/pwss/utils/MonitoredDirectoryUtils.java
rename to src/main/java/org/pwss/util/MonitoredDirectoryUtil.java
index 19e5dcf..73507b1 100644
--- a/src/main/java/org/pwss/utils/MonitoredDirectoryUtils.java
+++ b/src/main/java/org/pwss/util/MonitoredDirectoryUtil.java
@@ -1,7 +1,10 @@
-package org.pwss.utils;
+package org.pwss.util;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
+import java.util.LinkedList;
import java.util.List;
import org.pwss.model.entity.MonitoredDirectory;
import org.slf4j.Logger;
@@ -9,17 +12,17 @@
/**
* Utility class for handling operations related to monitored directories.
*/
-public final class MonitoredDirectoryUtils {
+public final class MonitoredDirectoryUtil {
/**
* Logger instance for logging purposes
*/
- private final static Logger log = org.slf4j.LoggerFactory.getLogger(MonitoredDirectoryUtils.class);
+ private final static Logger log = org.slf4j.LoggerFactory.getLogger(MonitoredDirectoryUtil.class);
/**
* Private constructor to prevent instantiation
*/
- private MonitoredDirectoryUtils() {
+ private MonitoredDirectoryUtil() {
}
/**
@@ -98,4 +101,25 @@ public static boolean isScanOlderThan1Minute(MonitoredDirectory dir) {
return lastScan.isBefore(oneMinuteAgo);
}
+
+ /**
+ * Filters a list of monitored directories based on whether their paths exist in
+ * the filesystem.
+ *
+ * @param inputList The list of monitored directories to be filtered.
+ * @return A new list containing only the directories from the input list that
+ * have valid, confirmed paths.
+ */
+ public static List filterMonitoredDirectoriesOnConfirmedPath(
+ List inputList) {
+
+ List mDirectories = new LinkedList<>();
+
+ for (MonitoredDirectory m : inputList) {
+ if (Files.exists(Path.of(m.path()))) {
+ mDirectories.add(m);
+ }
+ }
+ return mDirectories;
+ }
}
diff --git a/src/main/java/org/pwss/utils/OSUtils.java b/src/main/java/org/pwss/util/OSUtil.java
similarity index 97%
rename from src/main/java/org/pwss/utils/OSUtils.java
rename to src/main/java/org/pwss/util/OSUtil.java
index b86fa59..15da0ae 100644
--- a/src/main/java/org/pwss/utils/OSUtils.java
+++ b/src/main/java/org/pwss/util/OSUtil.java
@@ -1,13 +1,13 @@
-package org.pwss.utils;
+package org.pwss.util;
/**
* Utility class for operating system detection and information.
* Provides methods to determine the current OS type and related functionalities.
*/
-public final class OSUtils {
+public final class OSUtil {
// Private constructor to prevent instantiation
- private OSUtils() {
+ private OSUtil() {
throw new UnsupportedOperationException("Utility class cannot be instantiated");
}
@@ -105,7 +105,6 @@ public static final boolean isMac() {
* @return A warning message string specific to the current OS.
*/
public static String getQuarantineWarningMessage() {
- // TODO: Replace with custom, detailed warning messages for each OS.
return switch (determineOSType()) {
case WINDOWS ->
"Warning: Quarantining or removing Windows system files (drivers, DLLs, registry-related files) can render the system unstable or unbootable. Back up data, ensure you have recovery media and administrator access before proceeding.";
diff --git a/src/main/java/org/pwss/utils/ReportUtils.java b/src/main/java/org/pwss/util/ReportUtil.java
similarity index 98%
rename from src/main/java/org/pwss/utils/ReportUtils.java
rename to src/main/java/org/pwss/util/ReportUtil.java
index 687f88e..06a8dc2 100644
--- a/src/main/java/org/pwss/utils/ReportUtils.java
+++ b/src/main/java/org/pwss/util/ReportUtil.java
@@ -1,4 +1,4 @@
-package org.pwss.utils;
+package org.pwss.util;
import java.text.SimpleDateFormat;
import java.util.Date;
@@ -8,7 +8,7 @@
import org.pwss.model.entity.Scan;
import org.pwss.model.entity.ScanSummary;
-public class ReportUtils {
+public class ReportUtil {
/**
* Formats a ScanSummary into a human-readable string.
diff --git a/src/main/java/org/pwss/util/ScanUtil.java b/src/main/java/org/pwss/util/ScanUtil.java
new file mode 100644
index 0000000..4811055
--- /dev/null
+++ b/src/main/java/org/pwss/util/ScanUtil.java
@@ -0,0 +1,63 @@
+package org.pwss.util;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import org.pwss.model.entity.Scan;
+
+/**
+ * Utility class for constructing scan-related messages.
+ */
+public final class ScanUtil {
+
+ /**
+ * Prefix string used in scan completion messages indicating the number of
+ * differences found.
+ */
+ private static final String SCAN_COMPLETED_DIFFS_PREFIX = "Scan completed with ";
+
+ /**
+ * Suffix string used in scan completion messages providing additional
+ * information about viewing details.
+ */
+ private static final String SCAN_COMPLETED_DIFFS_SUFFIX = " differences found.\nDo you wish to see the details?\nYou can always view results later in the recent scans table.";
+
+ /**
+ * Private constructor to prevent instantiation of this utility class.
+ */
+ private ScanUtil() {
+ // Prevent instantiation
+ }
+
+ /**
+ * Constructs a scan completion message with the specified number of differences
+ * found.
+ *
+ * @param diffNumber The number of differences found during the scan.
+ * @return A string containing the complete scan message.
+ */
+ public static String constructDiffMessageString(long diffNumber) {
+ return SCAN_COMPLETED_DIFFS_PREFIX + diffNumber + SCAN_COMPLETED_DIFFS_SUFFIX;
+ }
+
+ /**
+ * Filters the provided list of scans to return a list containing only distinct
+ * scans based on their monitored directory IDs.
+ *
+ * @param scans The list of scans to filter.
+ * @return A list of scans with distinct monitored directory IDs.
+ */
+ public static List getScansDistinctByDirectory(List scans) {
+ return scans.stream()
+ .filter(distinctByKey(scan -> scan.monitoredDirectory().id()))
+ .toList();
+ }
+
+ // Helper method to create a predicate for distinct filtering based on a key extractor
+ private static Predicate distinctByKey(Function super T, ?> keyExtractor) {
+ Set