# JsonMaterialRepository.java

    package com.university.bookstore.repository;

    import java.io.File;
    import java.io.IOException;
    import java.nio.file.Path;
    import java.nio.file.Paths;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Optional;

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

    /**
     * JSON-based implementation of MaterialRepository (Adapter in hexagonal architecture).
     * Persists materials to a JSON file using Jackson for serialization.
     *
     * <p>This adapter implements the MaterialRepository port, providing JSON file-based
     * persistence without the domain layer knowing about the storage mechanism.</p>
     *
     * @author Navid Mohaghegh
     * @version 3.0
     * @since 2024-09-15
     */
    public class JsonMaterialRepository implements MaterialRepository {

        private static final String SAFE_BASE_DIR = System.getProperty("user.dir") + "/data";
        private static final int MAX_PATH_LENGTH = 255;

        private final String filePath;
        private final ObjectMapper objectMapper;
        private final File dataFile;

        /**
         * Creates a new JSON material repository.
         *
         * @param filePath the path to the JSON file for persistence
         */
        public JsonMaterialRepository(String filePath) {
            // Validate and sanitize the file path to prevent path traversal
            this.filePath = validateAndSanitizePath(filePath);
            this.objectMapper = new ObjectMapper();
            this.objectMapper.findAndRegisterModules();
            this.dataFile = new File(this.filePath);

            // Ensure the directory exists
            File parentDir = dataFile.getParentFile();
            if (parentDir != null && !parentDir.exists()) {
                parentDir.mkdirs();
            }
        }

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

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

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

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

            try {
                // Normalize the path and ensure it's within safe directory
                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.toString();
            } 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");
            }

            try {
                List<Material> materials = loadAll();

                // 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 to file using wrapper to ensure polymorphic serialization
                MaterialsWrapper wrapper = new MaterialsWrapper(materials);
                objectMapper.writerWithDefaultPrettyPrinter().writeValue(dataFile, wrapper);

            } catch (IOException e) {
                throw new RepositoryException("Failed to save material: " + material.getId(), e);
            }
        }

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

            try {
                List<Material> materials = loadAll();
                return materials.stream()
                    .filter(m -> id.equals(m.getId()))
                    .findFirst();

            } catch (IOException e) {
                throw new RepositoryException("Failed to find material by ID: " + id, e);
            }
        }

        @Override
        public List<Material> findAll() {
            try {
                return loadAll();
            } catch (IOException e) {
                throw new RepositoryException("Failed to load all materials", e);
            }
        }

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

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

                if (removed) {
                    MaterialsWrapper wrapper = new MaterialsWrapper(materials);
                    objectMapper.writerWithDefaultPrettyPrinter().writeValue(dataFile, wrapper);
                }

                return removed;

            } catch (IOException e) {
                throw new RepositoryException("Failed to delete material: " + id, e);
            }
        }

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

        @Override
        public long count() {
            try {
                return loadAll().size();
            } catch (IOException e) {
                throw new RepositoryException("Failed to count materials", e);
            }
        }

        @Override
        public void deleteAll() {
            try {
                MaterialsWrapper wrapper = new MaterialsWrapper(new ArrayList<Material>());
                objectMapper.writerWithDefaultPrettyPrinter().writeValue(dataFile, wrapper);
            } catch (IOException e) {
                throw new RepositoryException("Failed to clear all materials", e);
            }
        }

        /**
         * Loads all materials from the JSON file.
         *
         * @return list of materials
         * @throws IOException if file reading fails
         */
        private List<Material> loadAll() throws IOException {
            if (!dataFile.exists()) {
                return new ArrayList<>();
            }

            if (dataFile.length() == 0) {
                return new ArrayList<>();
            }

            try {
                MaterialsWrapper wrapper = objectMapper.readValue(dataFile, MaterialsWrapper.class);
                return wrapper.getMaterials() != null ? wrapper.getMaterials() : new ArrayList<>();
            } catch (IOException e) {
                // Log the error for debugging
                System.err.println("Failed to load materials from " + dataFile + ": " + e.getMessage());
                // If JSON parsing fails, return empty list
                return new ArrayList<>();
            }
        }

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

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

        /**
         * Gets the size of the data file in bytes.
         *
         * @return file size in bytes
         */
        public long getDataFileSize() {
            return dataFile.length();
        }
    }