# ModernJsonMaterialRepository.java

    package com.university.bookstore.repository;

    import java.io.*;
    import java.nio.file.Files;
    import java.nio.file.Path;
    import java.nio.file.Paths;
    import java.nio.file.StandardOpenOption;
    import java.util.*;
    import java.util.concurrent.locks.ReadWriteLock;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    import java.util.logging.Level;
    import java.util.logging.Logger;

    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.databind.SerializationFeature;
    import com.university.bookstore.model.Material;

    /**
     * Modern JSON-based implementation of MaterialRepository using best practices.
     * Features:
     * - Try-with-resources for automatic resource management
     * - NIO.2 for better file operations
     * - Thread-safe file operations with ReadWriteLock
     * - Better error handling and logging
     * - Atomic file operations to prevent corruption
     *
     * @author Navid Mohaghegh
     * @version 4.0
     * @since 2024-09-15
     */
    public class ModernJsonMaterialRepository implements MaterialRepository, AutoCloseable {

        private static final Logger LOGGER = Logger.getLogger(ModernJsonMaterialRepository.class.getName());
        private static final String SAFE_BASE_DIR = System.getProperty("user.dir") + "/data";
        private static final int MAX_PATH_LENGTH = 255;

        private final Path dataFile;
        private final Path backupFile;
        private final ObjectMapper objectMapper;
        private final ReadWriteLock fileLock;
        private volatile boolean closed = false;

        /**
         * Creates a new modern JSON material repository.
         *
         * @param filePath the path to the JSON file for persistence
         */
        public ModernJsonMaterialRepository(String filePath) {
            Path validatedPath = validateAndSanitizePath(filePath);
            this.dataFile = validatedPath;
            this.backupFile = Paths.get(validatedPath.toString() + ".backup");
            this.fileLock = new ReentrantReadWriteLock();

            this.objectMapper = new ObjectMapper();
            this.objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
            this.objectMapper.findAndRegisterModules(); // For Java 8 time support

            initializeStorage();
        }

        private void initializeStorage() {
            try {
                // Ensure the directory exists
                Path parentDir = dataFile.getParent();
                if (parentDir != null && !Files.exists(parentDir)) {
                    Files.createDirectories(parentDir);
                    LOGGER.info("Created directory: " + parentDir);
                }

                // Create empty file if it doesn't exist
                if (!Files.exists(dataFile)) {
                    saveAtomic(new ArrayList<>());
                    LOGGER.info("Created new data file: " + dataFile);
                }
            } catch (IOException e) {
                throw new RepositoryException("Failed to initialize storage", e);
            }
        }

        /**
         * Validates and sanitizes the file path to prevent path traversal attacks.
         *
         * @param filePath the file path to validate
         * @return the validated and sanitized Path
         * @throws SecurityException if the path is invalid or attempts path traversal
         */
        private Path validateAndSanitizePath(String filePath) {
            if (filePath == null || filePath.trim().isEmpty()) {
                throw new IllegalArgumentException("File path cannot be null or empty");
            }

            // Remove any path traversal attempts
            String cleanPath = filePath.replaceAll("\\.\\./", "").replaceAll("\\.\\.", "");

            // Check for suspicious patterns
            if (cleanPath.contains("../") || cleanPath.contains("..\\") ||
                cleanPath.contains("%2e%2e") || cleanPath.contains("%252e")) {
                throw new SecurityException("Invalid file path: potential path traversal detected");
            }

            // Validate path length
            if (cleanPath.length() > MAX_PATH_LENGTH) {
                throw new IllegalArgumentException("File path exceeds maximum length");
            }

            try {
                // Normalize the path
                Path normalizedPath = Paths.get(cleanPath).normalize();
                Path safePath = Paths.get(SAFE_BASE_DIR).normalize();

                // If the path is not absolute, make it relative to safe directory
                if (!normalizedPath.isAbsolute()) {
                    normalizedPath = safePath.resolve(normalizedPath).normalize();
                }

                // Ensure the normalized path is within the safe directory
                if (!normalizedPath.startsWith(safePath)) {
                    throw new SecurityException("File path must be within the safe directory: " + SAFE_BASE_DIR);
                }

                return normalizedPath;
            } catch (Exception e) {
                if (e instanceof SecurityException) {
                    throw (SecurityException) e;
                }
                throw new IllegalArgumentException("Invalid file path: " + e.getMessage(), e);
            }
        }

        @Override
        public void save(Material material) {
            if (material == null) {
                throw new IllegalArgumentException("Material cannot be null");
            }
            ensureNotClosed();

            fileLock.writeLock().lock();
            try {
                List<Material> materials = loadAllInternal();

                // Remove existing material with same ID if it exists
                materials.removeIf(m -> m.getId().equals(material.getId()));

                // Add the new/updated material
                materials.add(material);

                // Save atomically using wrapper
                saveAtomic(materials);

                LOGGER.fine("Saved material: " + material.getId());
            } catch (IOException e) {
                throw new RepositoryException("Failed to save material: " + material.getId(), e);
            } finally {
                fileLock.writeLock().unlock();
            }
        }

        /**
         * Saves multiple materials in batch for better performance.
         *
         * @param materialsToSave collection of materials to save
         */
        public void saveAll(Collection<Material> materialsToSave) {
            Objects.requireNonNull(materialsToSave, "Materials collection cannot be null");
            ensureNotClosed();

            if (materialsToSave.isEmpty()) {
                return;
            }

            fileLock.writeLock().lock();
            try {
                List<Material> materials = loadAllInternal();

                // Create a map for efficient lookup
                Map<String, Material> materialMap = new HashMap<>();
                for (Material m : materials) {
                    materialMap.put(m.getId(), m);
                }

                // Update or add new materials
                for (Material material : materialsToSave) {
                    if (material != null) {
                        materialMap.put(material.getId(), material);
                    }
                }

                // Save atomically
                saveAtomic(new ArrayList<>(materialMap.values()));

                LOGGER.fine("Saved " + materialsToSave.size() + " materials in batch");
            } catch (IOException e) {
                throw new RepositoryException("Failed to save materials batch", e);
            } finally {
                fileLock.writeLock().unlock();
            }
        }

        @Override
        public Optional<Material> findById(String id) {
            if (id == null || id.trim().isEmpty()) {
                return Optional.empty();
            }
            ensureNotClosed();

            fileLock.readLock().lock();
            try {
                List<Material> materials = loadAllInternal();
                return materials.stream()
                    .filter(m -> id.equals(m.getId()))
                    .findFirst();
            } catch (IOException e) {
                LOGGER.log(Level.WARNING, "Failed to find material by ID: " + id, e);
                return Optional.empty();
            } finally {
                fileLock.readLock().unlock();
            }
        }

        @Override
        public List<Material> findAll() {
            ensureNotClosed();

            fileLock.readLock().lock();
            try {
                return new ArrayList<>(loadAllInternal());
            } catch (IOException e) {
                LOGGER.log(Level.WARNING, "Failed to load all materials", e);
                return new ArrayList<>();
            } finally {
                fileLock.readLock().unlock();
            }
        }

        @Override
        public boolean delete(String id) {
            if (id == null || id.trim().isEmpty()) {
                return false;
            }
            ensureNotClosed();

            fileLock.writeLock().lock();
            try {
                List<Material> materials = loadAllInternal();
                boolean removed = materials.removeIf(m -> id.equals(m.getId()));

                if (removed) {
                    saveAtomic(materials);
                    LOGGER.fine("Deleted material: " + id);
                }

                return removed;
            } catch (IOException e) {
                throw new RepositoryException("Failed to delete material: " + id, e);
            } finally {
                fileLock.writeLock().unlock();
            }
        }

        /**
         * Deletes multiple materials in batch.
         *
         * @param ids collection of IDs to delete
         * @return number of materials deleted
         */
        public int deleteAll(Collection<String> ids) {
            if (ids == null || ids.isEmpty()) {
                return 0;
            }
            ensureNotClosed();

            fileLock.writeLock().lock();
            try {
                List<Material> materials = loadAllInternal();
                Set<String> idSet = new HashSet<>(ids);
                int originalSize = materials.size();

                materials.removeIf(m -> idSet.contains(m.getId()));
                int deleted = originalSize - materials.size();

                if (deleted > 0) {
                    saveAtomic(materials);
                    LOGGER.fine("Deleted " + deleted + " materials in batch");
                }

                return deleted;
            } catch (IOException e) {
                throw new RepositoryException("Failed to delete materials batch", e);
            } finally {
                fileLock.writeLock().unlock();
            }
        }

        @Override
        public boolean exists(String id) {
            return findById(id).isPresent();
        }

        @Override
        public long count() {
            ensureNotClosed();

            fileLock.readLock().lock();
            try {
                return loadAllInternal().size();
            } catch (IOException e) {
                LOGGER.log(Level.WARNING, "Failed to count materials", e);
                return 0;
            } finally {
                fileLock.readLock().unlock();
            }
        }

        @Override
        public void deleteAll() {
            ensureNotClosed();

            fileLock.writeLock().lock();
            try {
                saveAtomic(new ArrayList<>());
                LOGGER.info("Cleared all materials from repository");
            } catch (IOException e) {
                throw new RepositoryException("Failed to clear all materials", e);
            } finally {
                fileLock.writeLock().unlock();
            }
        }

        /**
         * Loads all materials from the JSON file using try-with-resources.
         *
         * @return list of materials
         * @throws IOException if file reading fails
         */
        private List<Material> loadAllInternal() throws IOException {
            if (!Files.exists(dataFile) || Files.size(dataFile) == 0) {
                return new ArrayList<>();
            }

            // Use try-with-resources for automatic resource management
            try (BufferedReader reader = Files.newBufferedReader(dataFile)) {
                MaterialsWrapper wrapper = objectMapper.readValue(reader, MaterialsWrapper.class);
                return wrapper.getMaterials() != null ? wrapper.getMaterials() : new ArrayList<>();
            } catch (IOException e) {
                // Try to restore from backup if main file is corrupted
                if (Files.exists(backupFile)) {
                    LOGGER.warning("Main file corrupted, attempting to restore from backup");
                    try (BufferedReader backupReader = Files.newBufferedReader(backupFile)) {
                        MaterialsWrapper wrapper = objectMapper.readValue(backupReader, MaterialsWrapper.class);
                        // Restore the main file from backup
                        Files.copy(backupFile, dataFile,
                            java.nio.file.StandardCopyOption.REPLACE_EXISTING);
                        return wrapper.getMaterials() != null ? wrapper.getMaterials() : new ArrayList<>();
                    } catch (IOException backupError) {
                        LOGGER.log(Level.SEVERE, "Failed to restore from backup", backupError);
                        return new ArrayList<>();
                    }
                }
                LOGGER.log(Level.WARNING, "Failed to parse JSON, returning empty list", e);
                return new ArrayList<>();
            }
        }

        /**
         * Saves materials atomically to prevent corruption.
         *
         * @param materials list of materials to save
         * @throws IOException if saving fails
         */
        private void saveAtomic(List<Material> materials) throws IOException {
            // Create backup of current file if it exists
            if (Files.exists(dataFile)) {
                Files.copy(dataFile, backupFile,
                    java.nio.file.StandardCopyOption.REPLACE_EXISTING);
            }

            // Write to temporary file first
            Path tempFile = Files.createTempFile(dataFile.getParent(), "temp", ".json");

            try {
                // Use try-with-resources for writing with wrapper
                try (BufferedWriter writer = Files.newBufferedWriter(tempFile,
                        StandardOpenOption.CREATE,
                        StandardOpenOption.TRUNCATE_EXISTING)) {
                    MaterialsWrapper wrapper = new MaterialsWrapper(materials);
                    objectMapper.writerWithDefaultPrettyPrinter().writeValue(writer, wrapper);
                }

                // Atomic move (rename) - this is atomic on most file systems
                Files.move(tempFile, dataFile,
                    java.nio.file.StandardCopyOption.REPLACE_EXISTING,
                    java.nio.file.StandardCopyOption.ATOMIC_MOVE);

            } catch (IOException e) {
                // Clean up temp file if operation failed
                try {
                    Files.deleteIfExists(tempFile);
                } catch (IOException deleteError) {
                    LOGGER.log(Level.WARNING, "Failed to delete temp file", deleteError);
                }
                throw e;
            }
        }

        /**
         * Creates a backup of the current data file.
         *
         * @return true if backup was created successfully
         */
        public boolean createBackup() {
            ensureNotClosed();

            fileLock.readLock().lock();
            try {
                if (Files.exists(dataFile)) {
                    Path backupPath = Paths.get(dataFile + "." + System.currentTimeMillis() + ".backup");
                    Files.copy(dataFile, backupPath);
                    LOGGER.info("Created backup: " + backupPath);
                    return true;
                }
                return false;
            } catch (IOException e) {
                LOGGER.log(Level.WARNING, "Failed to create backup", e);
                return false;
            } finally {
                fileLock.readLock().unlock();
            }
        }

        /**
         * Restores data from a backup file.
         *
         * @param backupPath path to the backup file
         * @return true if restore was successful
         */
        public boolean restoreFromBackup(String backupPath) {
            Objects.requireNonNull(backupPath, "Backup path cannot be null");
            ensureNotClosed();

            Path backup = Paths.get(backupPath);
            if (!Files.exists(backup)) {
                LOGGER.warning("Backup file does not exist: " + backupPath);
                return false;
            }

            fileLock.writeLock().lock();
            try {
                // Validate backup file can be read
                MaterialsWrapper wrapper;
                try (BufferedReader reader = Files.newBufferedReader(backup)) {
                    wrapper = objectMapper.readValue(reader, MaterialsWrapper.class);
                }

                // If validation successful, replace current file
                Files.copy(backup, dataFile,
                    java.nio.file.StandardCopyOption.REPLACE_EXISTING);

                LOGGER.info("Restored from backup: " + backupPath);
                return true;
            } catch (IOException e) {
                LOGGER.log(Level.SEVERE, "Failed to restore from backup: " + e.getMessage(), e);
                e.printStackTrace();
                return false;
            } finally {
                fileLock.writeLock().unlock();
            }
        }

        /**
         * Gets the file path used for persistence.
         *
         * @return the file path
         */
        public String getFilePath() {
            return dataFile.toString();
        }

        /**
         * Checks if the data file exists.
         *
         * @return true if the file exists
         */
        public boolean dataFileExists() {
            return Files.exists(dataFile);
        }

        /**
         * Gets the size of the data file in bytes.
         *
         * @return file size in bytes
         */
        public long getDataFileSize() {
            try {
                return Files.size(dataFile);
            } catch (IOException e) {
                return 0;
            }
        }

        /**
         * Performs maintenance operations like cleanup and optimization.
         */
        public void performMaintenance() {
            ensureNotClosed();

            fileLock.writeLock().lock();
            try {
                // Clean up old backup files
                Path parent = dataFile.getParent();
                if (parent != null) {
                    long cutoffTime = System.currentTimeMillis() - (7L * 24 * 60 * 60 * 1000); // 7 days

                    Files.list(parent)
                        .filter(path -> path.toString().endsWith(".backup"))
                        .filter(path -> {
                            try {
                                return Files.getLastModifiedTime(path).toMillis() < cutoffTime;
                            } catch (IOException e) {
                                return false;
                            }
                        })
                        .forEach(path -> {
                            try {
                                Files.delete(path);
                                LOGGER.fine("Deleted old backup: " + path);
                            } catch (IOException e) {
                                LOGGER.warning("Failed to delete old backup: " + path);
                            }
                        });
                }

                // Compact the JSON file by rewriting it
                List<Material> materials = loadAllInternal();
                saveAtomic(materials);

                LOGGER.info("Maintenance completed");
            } catch (IOException e) {
                LOGGER.log(Level.WARNING, "Maintenance failed", e);
            } finally {
                fileLock.writeLock().unlock();
            }
        }

        private void ensureNotClosed() {
            if (closed) {
                throw new IllegalStateException("Repository has been closed");
            }
        }

        @Override
        public void close() {
            if (!closed) {
                closed = true;
                LOGGER.info("Repository closed");
            }
        }
    }