diff --git a/.github/maven-settings.xml b/.github/maven-settings.xml index 735070f..bf03f3a 100644 --- a/.github/maven-settings.xml +++ b/.github/maven-settings.xml @@ -1,5 +1,5 @@ - diff --git a/.github/workflows/quality_and_tests.yml b/.github/workflows/quality_and_tests.yml index 4160574..def02e2 100644 --- a/.github/workflows/quality_and_tests.yml +++ b/.github/workflows/quality_and_tests.yml @@ -30,5 +30,12 @@ jobs: restore-keys: | ${{ runner.os }}-maven - - name: Build and Run Tests - run: mvn -B clean test + - name: Run Tests with Coverage Enforcement + run: mvn -B clean verify + + - name: Upload JaCoCo Report (Optional) + if: always() + uses: actions/upload-artifact@v4 + with: + name: jacoco-report + path: target/site/jacoco/index.html \ No newline at end of file diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml new file mode 100644 index 0000000..d22715d --- /dev/null +++ b/.idea/dictionaries/project.xml @@ -0,0 +1,7 @@ + + + + jrxml + + + \ No newline at end of file diff --git a/README.md b/README.md index ed16ae8..250a1ee 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,15 @@ # Unified Data Parser and Report Generator -This project provides a **pluggable**, **extensible**, and **framework-agnostic** solution to convert structured input data formats like **XLSX**, **CSV**, **JSON**, and **ByteStream** into a **Unified Data Format**, which can then be used to generate reports in **PDF**, **HTML**, or **XML** using **JasperReports** or similar tools. - +This project provides a **pluggable**, **extensible**, and **framework-agnostic** solution to convert structured input +data formats like **XLSX**, **CSV**, **JSON**, and **ByteStream** into a **Unified Data Format**, which can then be used +to generate reports in **PDF**, **HTML**, or **XML** using **JasperReports** or similar tools. ### 📦 Package & Build Status [![Maven Package](https://img.shields.io/badge/github--package-0.0.1--SNAPSHOT-blue)](https://github.com/docflex/UnifiedReporter/packages) - [![Build & Tests](https://github.com/docflex/UnifiedReporter/actions/workflows/build.yml/badge.svg)](https://github.com/docflex/UnifiedReporter/actions/workflows/build.yml) - - - ## Architecture Overview ### 🔁 Proposed Flow & Design @@ -59,7 +56,7 @@ This project provides a **pluggable**, **extensible**, and **framework-agnostic* ## 🏗 Project Modules | Module | Purpose | -| ------------------ | --------------------------------------------- | +|--------------------|-----------------------------------------------| | `formats` | Parsers for CSV, XLSX, JSON, ByteStream | | `common` | Error codes, exceptions, and utility classes | | `jasper-engine` | Report rendering using JasperReports | @@ -73,12 +70,14 @@ This project provides a **pluggable**, **extensible**, and **framework-agnostic* ### ✅ As a **Library** ```java -try (InputStream is = new FileInputStream("/path/to/file.xlsx")) { - UnifiedFormat format = new XLSXFormat(is, "MySource"); - List> data = format.getDataRows(); - - // pass to Jasper engine or use for validation - documentCreator.generatePdf(data, templatePath); +try(InputStream is = new FileInputStream("/path/to/file.xlsx")){ +UnifiedFormat format = new XLSXFormat(is, "MySource"); +List> data = format.getDataRows(); + +// pass to Jasper engine or use for validation + documentCreator. + +generatePdf(data, templatePath); } ``` @@ -97,8 +96,15 @@ try (InputStream is = new FileInputStream("/path/to/file.xlsx")) { ```java public interface UnifiedFormat { List> getDataRows(); - default List getColumnOrder() { return null; } - default String getSourceName() { return "unnamed"; } + + default List getColumnOrder() { + return null; + } + + default String getSourceName() { + return "unnamed"; + } + default void validateFields(List requiredColumns) throws FormatException; } ``` @@ -136,6 +142,7 @@ Unit tests are written using **JUnit 5** with in-memory test files for: * Field validation ```java + @Test void testValidXlsxParsing() { InputStream inputStream = getClass().getResourceAsStream("/valid.xlsx"); @@ -154,9 +161,9 @@ If required, you can introduce fluent builders for complex configuration like: ```java UnifiedFormat format = XLSXFormat.builder() - .withInputStream(stream) - .withSourceName("Sheet1") - .build(); + .withInputStream(stream) + .withSourceName("Sheet1") + .build(); ``` --- diff --git a/pom.xml b/pom.xml index 553caf3..c9f553a 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,5 @@ - @@ -49,6 +49,10 @@ 6.21.0 5.9 UTF-8 + UTF-8 + UTF-8 + 17 + 17 @@ -59,18 +63,18 @@ - - - org.springframework - spring-context - ${spring.version} - net.sf.jasperreports jasperreports ${jasperreports.version} + + + commons-logging + commons-logging + + @@ -87,11 +91,19 @@ 5.4.0 - + - org.springframework - spring-aspects - 6.1.6 + org.apache.poi + poi + 5.4.0 + + + + + org.junit.jupiter + junit-jupiter-api + 5.10.0 + test @@ -102,13 +114,6 @@ 6.2.8 - - - org.slf4j - slf4j-api - 2.0.13 - - org.apache.logging.log4j @@ -146,6 +151,14 @@ test + + org.mockito + mockito-core + 5.10.0 + test + + + commons-io @@ -156,6 +169,45 @@ + + org.jacoco + jacoco-maven-plugin + 0.8.11 + + + + prepare-agent + + + + report + verify + + report + + + + check + + check + + + + + BUNDLE + + + LINE + COVEREDRATIO + 0.80 + + + + + + + + maven-resources-plugin 3.3.1 diff --git a/src/main/java/org/unified/ReportGenerator.java b/src/main/java/org/unified/ReportGenerator.java new file mode 100644 index 0000000..6f0f429 --- /dev/null +++ b/src/main/java/org/unified/ReportGenerator.java @@ -0,0 +1,107 @@ +package org.unified; + +import lombok.extern.slf4j.Slf4j; +import net.sf.jasperreports.engine.JasperReport; +import org.unified.common.enums.FileExportFormat; +import org.unified.common.exceptions.ReportException; +import org.unified.formats.UnifiedFormat; +import org.unified.utils.ReportExporter; +import org.unified.utils.ReportValidators; + +import java.io.InputStream; +import java.util.Map; + +/** + * Central class responsible for generating reports from structured data and Jasper templates. + *

+ * This utility orchestrates the validation of input data, compilation/loading of report templates, + * and final export of reports into multiple formats (PDF, XLSX, HTML, XML). + *

+ * It abstracts the complexity behind parsing data, compiling templates, and exporting formats, + * providing a single entry point for report generation. + * + *

Supported Input

+ * This class currently expects a single data source: + *
    + *
  • {@link UnifiedFormat} object - a normalized wrapper over parsed tabular input (e.g., XLSX, CSV)
  • + *
+ * Other input types like {@code InputStream} or {@code byte[]} are not yet supported for dynamic format inference. + * + *

Usage Example

+ *
{@code
+ * InputStream templateStream = getClass().getResourceAsStream("/invoice_template.jasper");
+ * Map parameters = Map.of("CompanyName", "ACME Corp.");
+ *
+ * UnifiedFormat file = new XLSXFormat(myExcelStream, "InvoiceData");
+ *
+ * byte[] pdfBytes = ReportGenerator.generateReport(
+ *     file,
+ *     templateStream,
+ *     parameters,
+ *     FileExportFormat.PDF
+ * );
+ *
+ * Files.write(Paths.get("invoice.pdf"), pdfBytes);
+ * }
+ * + *

Exceptions

+ * This method throws {@link ReportException} for known validation or export issues, + * and wraps unexpected errors in a {@link RuntimeException}. + * + * @author Unified + */ +@Slf4j +public class ReportGenerator { + + /** + * Generates a Jasper report from structured tabular input using the specified template and export format. + *

+ * This method follows a 3-step process: + *

    + *
  1. Validates and extracts data from the input file
  2. + *
  3. Validates and compiles (or loads) the Jasper template
  4. + *
  5. Exports the filled report into the requested output format
  6. + *
+ * + * @param file A valid {@link UnifiedFormat} instance (e.g., XLSXFormat) + * @param jasperReportTemplateStream Input stream of the Jasper template (.jasper or .jrxml) + * @param additionalReportParameters Additional parameters for the Jasper report (e.g., metadata, dynamic values) + * @param exportFormat Desired file export format (PDF, XLSX, HTML, XML) + * @return A byte array representing the generated report file + * @throws ReportException if the input is invalid, template fails to load, or export fails + * @throws RuntimeException if an unexpected error occurs during generation + */ + public static byte[] generateReport( + Object file, + InputStream jasperReportTemplateStream, + Map additionalReportParameters, + FileExportFormat exportFormat + ) { + long startTime = System.nanoTime(); + + try { + UnifiedFormat inputFile = ReportValidators.validateInputFile(file); + + JasperReport reportTemplate = ReportValidators.validateJasperReport(jasperReportTemplateStream); + + byte[] output = ReportExporter.export( + inputFile.getDataRows(), + reportTemplate, + additionalReportParameters, + exportFormat + ); + + long endTime = System.nanoTime(); + long durationMillis = (endTime - startTime) / 1_000_000; + + log.info("✅ Report generated successfully in {} ms (Format: {})", durationMillis, exportFormat); + return output; + + } catch (ReportException rex) { + throw rex; + } catch (Exception e) { + log.error("❌ Unexpected error during report generation", e); + throw new RuntimeException("Report generation failed", e); + } + } +} diff --git a/src/main/java/org/unified/common/enums/ErrorCode.java b/src/main/java/org/unified/common/enums/ErrorCode.java index fd981ed..2e0749f 100644 --- a/src/main/java/org/unified/common/enums/ErrorCode.java +++ b/src/main/java/org/unified/common/enums/ErrorCode.java @@ -4,50 +4,163 @@ import lombok.Getter; import org.springframework.http.HttpStatus; +/** + * Enum representing structured error codes used across the application. + * Each error code includes: + *
    + *
  • A unique identifier (e.g., CSV_001)
  • + *
  • A human-readable error message
  • + *
  • An associated HTTP status code
  • + *
+ */ @Getter @AllArgsConstructor public enum ErrorCode { - // CSV ERRORS + + // ================= CSV ERRORS ================= + + /** + * Error when CSV parsing fails due to malformed content. + */ CSV_PARSE_ERROR("CSV_001", "Failed to parse CSV file", HttpStatus.BAD_REQUEST), + + /** + * Error when reading the CSV input fails. + */ CSV_IO_ERROR("CSV_002", "Failed to read CSV input", HttpStatus.UNPROCESSABLE_ENTITY), + + /** + * Error when the number of columns in a row doesn't match the header. + */ CSV_ROW_COLUMN_MISMATCH("CSV_003", "Issue with Row Column Mismatch", HttpStatus.NOT_ACCEPTABLE), + + /** + * Error when the CSV file does not have a header row. + */ CSV_HEADER_MISSING("CSV_004", "CSV header is missing or null", HttpStatus.BAD_REQUEST), + + /** + * Error when the CSV header is present but has no valid column names. + */ CSV_HEADER_EMPTY("CSV_005", "CSV header does not contain any valid columns", HttpStatus.BAD_REQUEST), + + /** + * Error when the CSV header contains duplicate column names. + */ CSV_HEADER_DUPLICATE("CSV_006", "CSV header contains duplicate columns", HttpStatus.BAD_REQUEST), + // ================= XLSX ERRORS ================= - // XLSX ERRORS + /** + * Error when parsing the Excel file fails. + */ XLSX_PARSE_ERROR("XLSX_001", "Failed to read Excel file", HttpStatus.BAD_REQUEST), + + /** + * Error when the Excel file is completely empty. + */ XLSX_MISSING_HEADERS("XLSX_002", "XLSX file is empty.", HttpStatus.NO_CONTENT), + + /** + * Error when the Excel header contains null or blank cells. + */ XLSX_NULL_HEADER("XLSX_003", "Found blank or null header.", HttpStatus.PARTIAL_CONTENT), + + /** + * Error when the Excel header contains duplicate column names. + */ XLSX_DUPLICATE_HEADER("XLSX_004", "Duplicate header Found.", HttpStatus.NOT_ACCEPTABLE), + + /** + * Error when an Excel header has an unsupported or invalid type. + */ XLSX_INVALID_HEADER_TYPE("XLSX_005", "Invalid Header Found.", HttpStatus.NOT_ACCEPTABLE), - // JSON ERRORS + // ================= JSON ERRORS ================= + + /** + * Error when the JSON input is malformed or contains syntax issues. + */ JSON_SYNTAX_ERROR("JSON_001", "Malformed JSON input", HttpStatus.BAD_REQUEST), + + /** + * Error when the JSON structure cannot be mapped to the expected format. + */ JSON_MAPPING_ERROR("JSON_002", "Could not map JSON to expected structure", HttpStatus.UNPROCESSABLE_ENTITY), - // BYTE STREAM ERRORS + // ================= BYTE STREAM ERRORS ================= + + /** + * Error when the byte stream format is not recognized or unsupported. + */ BYTE_UNSUPPORTED_FORMAT("BYTE_001", "Unsupported byte stream format", HttpStatus.UNSUPPORTED_MEDIA_TYPE), + + /** + * Error when decoding a byte stream fails. + */ BYTE_DECODE_ERROR("BYTE_002", "Failed to decode byte stream", HttpStatus.BAD_REQUEST), - // GENERIC + // ================= GENERIC ERRORS ================= + + /** + * Fallback error for unknown or unexpected situations. + */ UNKNOWN_ERROR("GEN_001", "An unexpected error occurred", HttpStatus.INTERNAL_SERVER_ERROR), + + /** + * Error when an I/O exception occurs while processing input. + */ IO_EXCEPTION("GEN_002", "IO Exception Occured for Input", HttpStatus.NOT_FOUND), - // REPORT GENERATION ERRORS + // ================= REPORT GENERATION ERRORS ================= + + /** + * Error when the report template stream is null. + */ REPORT_TEMPLATE_NULL("REP_001", "Report template InputStream is null", HttpStatus.BAD_REQUEST), + + /** + * Error when compiling the JasperReports .jrxml template fails. + */ REPORT_TEMPLATE_COMPILE_FAILED("REP_002", "Failed to compile .jrxml template", HttpStatus.UNPROCESSABLE_ENTITY), + + /** + * Error when loading a compiled Jasper .jasper file fails. + */ REPORT_TEMPLATE_LOAD_FAILED("REP_003", "Failed to load compiled .jasper template", HttpStatus.UNPROCESSABLE_ENTITY), + + /** + * Error when the input data for report generation is missing or empty. + */ REPORT_DATA_EMPTY("REP_004", "Input data list is empty or null", HttpStatus.NO_CONTENT), + + /** + * Error when filling the report with data and parameters fails. + */ REPORT_FILL_FAILED("REP_005", "Failed to fill the report with provided data and parameters", HttpStatus.INTERNAL_SERVER_ERROR), + + /** + * Error when exporting the report fails due to I/O or formatting issues. + */ REPORT_EXPORT_FAILED("REP_006", "Failed to export the report to the selected format", HttpStatus.INTERNAL_SERVER_ERROR), - REPORT_OUTPUT_PATH_INVALID("REP_007", "Output path is null or inaccessible", HttpStatus.BAD_REQUEST), - REPORT_FORMAT_UNSUPPORTED("REP_008", "Unsupported export format", HttpStatus.NOT_ACCEPTABLE), - REPORT_DIRECTORY_CREATE_FAILED("REP_009", "Failed to create directory for output path", HttpStatus.INTERNAL_SERVER_ERROR); + /** + * Error when an unsupported report export format is specified. + */ + REPORT_FORMAT_UNSUPPORTED("REP_007", "Unsupported export format", HttpStatus.NOT_ACCEPTABLE); + /** + * A unique string code identifying the error. + */ private final String code; + + /** + * A human-readable error message describing the issue. + */ private final String message; + + /** + * The HTTP status code associated with the error. + */ private final HttpStatus httpStatus; } diff --git a/src/main/java/org/unified/common/enums/FileExportFormat.java b/src/main/java/org/unified/common/enums/FileExportFormat.java index e98e5e9..49002a5 100644 --- a/src/main/java/org/unified/common/enums/FileExportFormat.java +++ b/src/main/java/org/unified/common/enums/FileExportFormat.java @@ -1,9 +1,38 @@ package org.unified.common.enums; +/** + * Enum representing supported export formats for reports. + * These formats determine the type of output generated by the reporting engine. + */ public enum FileExportFormat { + + /** + * Export the report as a PDF document. + */ PDF, + + /** + * Export the report as an HTML page. + */ HTML, + + /** + * Export the report as an Excel spreadsheet (.xlsx). + */ XLSX, + + /** + * Export the report as an XML file. + */ + XML, + + /** + * Export the report as a Word document (.docx). + */ DOCX, - CSV, -} \ No newline at end of file + + /** + * Export the report as a CSV (Comma-Separated Values) file. + */ + CSV +} diff --git a/src/main/java/org/unified/common/exceptions/FormatException.java b/src/main/java/org/unified/common/exceptions/FormatException.java index c0d1d25..b6dfa53 100644 --- a/src/main/java/org/unified/common/exceptions/FormatException.java +++ b/src/main/java/org/unified/common/exceptions/FormatException.java @@ -4,15 +4,72 @@ import lombok.extern.slf4j.Slf4j; import org.unified.common.enums.ErrorCode; +/** + * Custom runtime exception used to handle structured format-related errors + * in parsing or exporting operations such as CSV, XLSX, JSON, etc. + *

+ * Each exception is associated with an {@link ErrorCode} that describes the nature + * of the error and the appropriate HTTP status for RESTful response handling. + */ @Getter @Slf4j public class FormatException extends RuntimeException { + + /** + * The structured error code representing the type of format exception. + */ private final ErrorCode errorCode; + /** + * Constructs a new {@code FormatException} with the given error code. + * This constructor is used when no underlying exception is present. + * + * @param errorCode the specific {@link ErrorCode} representing the error + */ + public FormatException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + logError(null); + } + + /** + * Constructs a new {@code FormatException} with the given error code + * and the original cause of the error. + * + * @param errorCode the specific {@link ErrorCode} representing the error + * @param parentException the underlying cause of this exception + */ public FormatException(ErrorCode errorCode, Exception parentException) { - super(parentException + "\nDescription:\n" + errorCode.getMessage()); - log.error("Format Exception Occurred!"); - log.error("Error Code: {} | Error Message: {} | Error Status: {}", errorCode.getCode(), errorCode.getMessage(), errorCode.getHttpStatus()); + super(buildMessage(errorCode, parentException), parentException); this.errorCode = errorCode; + logError(parentException); + } + + /** + * Builds a detailed error message including the cause and description. + * + * @param errorCode the error code associated with this exception + * @param parent the underlying exception + * @return a formatted error message + */ + private static String buildMessage(ErrorCode errorCode, Exception parent) { + if (parent == null) { + return errorCode.getMessage(); + } + return parent + "\nDescription:\n" + errorCode.getMessage(); + } + + /** + * Logs error details to the logger. + * + * @param parentException the original exception, if any + */ + private void logError(Exception parentException) { + log.error("Report Exception Occurred!"); + log.error("Error Code: {} | Error Message: {} | HTTP Status: {}", + errorCode.getCode(), errorCode.getMessage(), errorCode.getHttpStatus()); + if (parentException != null) { + log.error("Cause: ", parentException); + } } } diff --git a/src/main/java/org/unified/common/exceptions/ReportException.java b/src/main/java/org/unified/common/exceptions/ReportException.java index 2f3c938..f6239da 100644 --- a/src/main/java/org/unified/common/exceptions/ReportException.java +++ b/src/main/java/org/unified/common/exceptions/ReportException.java @@ -4,23 +4,53 @@ import lombok.extern.slf4j.Slf4j; import org.unified.common.enums.ErrorCode; +/** + * Custom runtime exception used to represent errors related to report generation + * (e.g., loading templates, compiling, filling, or exporting reports). + *

+ * Each exception wraps an {@link ErrorCode} that provides detailed context about the issue, + * including an appropriate HTTP status for RESTful responses. + */ @Getter @Slf4j public class ReportException extends RuntimeException { + + /** + * The structured error code describing the type of report-related error. + */ private final ErrorCode errorCode; + /** + * Constructs a new {@code ReportException} using only an error code. + * Use this when there is no underlying cause. + * + * @param errorCode the specific {@link ErrorCode} that describes the error + */ public ReportException(ErrorCode errorCode) { super(errorCode.getMessage()); this.errorCode = errorCode; logError(null); } + /** + * Constructs a new {@code ReportException} with an error code and a cause. + * + * @param errorCode the specific {@link ErrorCode} that describes the error + * @param parentException the underlying exception that caused this error + */ public ReportException(ErrorCode errorCode, Exception parentException) { super(buildMessage(errorCode, parentException), parentException); this.errorCode = errorCode; logError(parentException); } + /** + * Builds a complete exception message including the parent exception's message. + * + * @param errorCode the associated error code + * @param parent the underlying exception (if any) + * @return a detailed error message + */ private static String buildMessage(ErrorCode errorCode, Exception parent) { if (parent == null) { return errorCode.getMessage(); @@ -28,6 +58,11 @@ private static String buildMessage(ErrorCode errorCode, Exception parent) { return parent + "\nDescription:\n" + errorCode.getMessage(); } + /** + * Logs the error details using SLF4J. + * + * @param parentException the original exception, if present + */ private void logError(Exception parentException) { log.error("Report Exception Occurred!"); log.error("Error Code: {} | Error Message: {} | HTTP Status: {}", diff --git a/src/main/java/org/unified/config/AppConfig.java b/src/main/java/org/unified/config/AppConfig.java deleted file mode 100644 index 6f21c73..0000000 --- a/src/main/java/org/unified/config/AppConfig.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.unified.config; - -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; - -@Configuration -@ComponentScan(basePackages = "org.unified") -public class AppConfig { -} diff --git a/src/main/java/org/unified/formats/CSVFormat.java b/src/main/java/org/unified/formats/CSVFormat.java index 9db73fd..061212e 100644 --- a/src/main/java/org/unified/formats/CSVFormat.java +++ b/src/main/java/org/unified/formats/CSVFormat.java @@ -6,11 +6,20 @@ import org.unified.common.enums.ErrorCode; import org.unified.common.exceptions.FormatException; +import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.*; +/** + * A parser class that implements {@link UnifiedFormat} for handling CSV files. + *

+ * It extracts header information and rows into a list of maps, + * while enforcing header uniqueness and row-column alignment. + *

+ * UTF-8 encoding is enforced with BOM stripping to handle multi-platform CSV files. + */ @Slf4j public class CSVFormat implements UnifiedFormat { @@ -18,27 +27,56 @@ public class CSVFormat implements UnifiedFormat { private final List columnOrder = new ArrayList<>(); private final String sourceName; + /** + * Constructs a new CSVFormat parser instance from an {@link InputStream}. + * + * @param csvStream the input stream containing the CSV content + * @param sourceName the name of the CSV source, used in logs; defaults to "CSV" if null + */ public CSVFormat(InputStream csvStream, String sourceName) { this.sourceName = sourceName != null ? sourceName : "CSV"; parse(csvStream); } + /** + * Returns the list of parsed data rows from the CSV file. + * Each row is represented as a {@link Map} with header names as keys. + * + * @return list of row data as maps + */ @Override public List> getDataRows() { return dataRows; } + /** + * Returns the ordered list of column headers as defined in the CSV header line. + * + * @return list of column headers + */ @Override public List getColumnOrder() { return columnOrder; } + /** + * Returns the logical name of the CSV source file. + * + * @return the source name + */ @Override public String getSourceName() { return sourceName; } - public void parse(InputStream inputStream) { + /** + * Parses the CSV input stream, extracting headers and row data. + * Validates the headers and each row during parsing. + * + * @param inputStream the input stream to parse + * @throws FormatException if parsing fails due to IO issues or malformed structure + */ + private void parse(InputStream inputStream) { log.info("Starting Parsing CSV ---> UnifiedFormat"); try ( BOMInputStream bomInputStream = BOMInputStream.builder().setInputStream(inputStream).get(); @@ -50,28 +88,37 @@ public void parse(InputStream inputStream) { extractHeadersFromCSV(headerLine); processRowsFromCSV(csvReader); + } catch (IOException e) { + throw new FormatException(ErrorCode.CSV_IO_ERROR, e); } catch (FormatException e) { + log.error("❌ Format error while parsing CSV", e); throw e; } catch (Exception e) { + log.error("❌ Unexpected error while parsing CSV", e); throw new FormatException(ErrorCode.CSV_PARSE_ERROR, e); } } // Utility Functions + /** + * Extracts and validates the header line from the CSV. + * Ensures headers are not empty or duplicated. + * + * @param headerLine the first line of the CSV, expected to contain headers + * @throws FormatException if headers are missing, empty, or duplicated + */ private void extractHeadersFromCSV(String[] headerLine) { - if (headerLine == null || headerLine.length == 0) { - throw new FormatException(ErrorCode.CSV_HEADER_MISSING, new Exception("CSV header is missing or empty")); - } - Set seen = new HashSet<>(); for (String header : headerLine) { String trimmed = header.trim(); if (trimmed.isEmpty()) { - throw new FormatException(ErrorCode.CSV_HEADER_EMPTY, new Exception("CSV header contains empty column names")); + log.error("❌ CSV header contains an empty column"); + throw new FormatException(ErrorCode.CSV_HEADER_EMPTY); } if (!seen.add(trimmed)) { - throw new FormatException(ErrorCode.CSV_HEADER_DUPLICATE, new Exception("CSV header contains duplicate column: " + trimmed)); + log.error("❌ Duplicate header found: {}", trimmed); + throw new FormatException(ErrorCode.CSV_HEADER_DUPLICATE); } columnOrder.add(trimmed); } @@ -79,6 +126,12 @@ private void extractHeadersFromCSV(String[] headerLine) { log.info("Extracted headers: {}", columnOrder); } + /** + * Reads and processes all data rows in the CSV, mapping them to the corresponding headers. + * + * @param reader the CSVReader positioned after the header + * @throws FormatException if any row has a mismatch in column count or processing fails + */ private void processRowsFromCSV(CSVReader reader) { try { int lineNumber = 2; @@ -98,22 +151,31 @@ private void processRowsFromCSV(CSVReader reader) { } catch (FormatException e) { throw e; } catch (Exception e) { + log.error("❌ Error processing CSV rows", e); throw new FormatException(ErrorCode.CSV_PARSE_ERROR, e); } } + /** + * Validates a CSV row: + * - Skips if empty or blank. + * - Throws if column count doesn't match the header size. + * + * @param row the row values + * @param lineNumber the current line number (used in logs) + * @return {@code true} if the row is valid, {@code false} if empty and should be skipped + * @throws FormatException for column count mismatches + */ private boolean validateCSVRow(String[] row, int lineNumber) { if (row.length == 0 || Arrays.stream(row).allMatch(String::isBlank)) { - log.warn("Skipping empty row at line {}", lineNumber); + log.warn("⚠️ Skipping empty row at line {}", lineNumber); return false; } if (row.length != columnOrder.size()) { - throw new FormatException( - ErrorCode.CSV_ROW_COLUMN_MISMATCH, - new Exception(String.format("CSV row at line %d has %d columns, expected %d", - lineNumber, row.length, columnOrder.size())) - ); + String msg = String.format("CSV row at line %d has %d columns, expected %d", lineNumber, row.length, columnOrder.size()); + log.error("❌ Row-column mismatch: {}", msg); + throw new FormatException(ErrorCode.CSV_ROW_COLUMN_MISMATCH); } return true; diff --git a/src/main/java/org/unified/formats/XLSXFormat.java b/src/main/java/org/unified/formats/XLSXFormat.java index 4308209..2d0d3af 100644 --- a/src/main/java/org/unified/formats/XLSXFormat.java +++ b/src/main/java/org/unified/formats/XLSXFormat.java @@ -9,6 +9,12 @@ import java.io.InputStream; import java.util.*; +/** + * A parser class that implements {@link UnifiedFormat} for reading Excel (XLSX) files. + *

+ * This class uses Apache POI to extract data from the first sheet of an XLSX file. + * It validates headers and processes each row into a list of maps. + */ @Slf4j public class XLSXFormat implements UnifiedFormat { @@ -16,27 +22,56 @@ public class XLSXFormat implements UnifiedFormat { private final List columnOrder = new ArrayList<>(); private final String sourceName; + /** + * Constructs an {@code XLSXFormat} parser from an {@link InputStream}. + * + * @param xlsxStream the input stream of the XLSX file + * @param sourceName optional logical name for the file (used in logs); defaults to "XLSX" if null + */ public XLSXFormat(InputStream xlsxStream, String sourceName) { this.sourceName = sourceName != null ? sourceName : "XLSX"; parse(xlsxStream); } + /** + * Returns the list of parsed data rows from the XLSX sheet. + * Each row is represented as a {@link Map} with headers as keys. + * + * @return list of data rows + */ @Override public List> getDataRows() { return dataRows; } + /** + * Returns the column order as defined in the Excel header row. + * + * @return list of header names + */ @Override public List getColumnOrder() { return columnOrder; } + /** + * Returns the name of the source Excel file. + * + * @return source name + */ @Override public String getSourceName() { return sourceName; } - public void parse(InputStream inputStream) { + /** + * Parses the XLSX input stream using Apache POI. + * Extracts headers and rows, and populates internal data structures. + * + * @param inputStream the input stream of the XLSX file + * @throws FormatException if parsing fails due to invalid structure or I/O error + */ + private void parse(InputStream inputStream) { log.info("Starting Parsing XLSX ---> UnifiedFormat"); try (Workbook workbook = new XSSFWorkbook(inputStream)) { Sheet sheet = workbook.getSheetAt(0); // or by name @@ -46,15 +81,23 @@ public void parse(InputStream inputStream) { processRowsFromExcel(rowIterator); } catch (FormatException e) { + log.error("❌ Format error while parsing XLSX file", e); throw e; } catch (Exception e) { + log.error("❌ Unexpected error while parsing XLSX file", e); throw new FormatException(ErrorCode.XLSX_PARSE_ERROR, e); } } // Utility Functions - private Object getCellValue(Cell cell) { + /** + * Extracts the value from an Excel cell based on its type. + * + * @param cell the cell from which to extract value + * @return the extracted Java object value (String, Double, Boolean, Date, or null) + */ + Object getCellValue(Cell cell) { if (cell == null) return null; return switch (cell.getCellType()) { @@ -68,8 +111,16 @@ private Object getCellValue(Cell cell) { }; } + /** + * Extracts headers from the first row of the Excel sheet. + * Validates for text type, non-blank values, and uniqueness. + * + * @param rowIterator iterator starting at the first row of the sheet + * @throws FormatException if headers are missing, duplicated, or invalid + */ private void extractHeadersFromExcel(Iterator rowIterator) { if (!rowIterator.hasNext()) { + log.error("❌ Missing headers: sheet is empty"); throw new FormatException(ErrorCode.XLSX_MISSING_HEADERS, new Exception("Missing headers: the sheet is empty")); } Row headerRow = rowIterator.next(); @@ -83,6 +134,13 @@ private void extractHeadersFromExcel(Iterator rowIterator) { log.info("Extracted headers: {}", columnOrder); } + /** + * Validates and extracts each row of data from the Excel sheet. + * Adds them as maps to the internal list of data rows. + * + * @param rowIterator iterator positioned after the header row + * @throws FormatException if processing fails + */ private void processRowsFromExcel(Iterator rowIterator) { int rowIndex = 1; while (rowIterator.hasNext()) { @@ -99,20 +157,37 @@ private void processRowsFromExcel(Iterator rowIterator) { } } + /** + * Validates the header cell: + *

    + *
  • Must be of type {@code STRING}
  • + *
  • Must not be blank
  • + *
  • Must be unique
  • + *
+ * + * @param header the header cell to validate + * @param index the column index (used for logging) + * @throws FormatException if any condition fails + */ private void validateExcelHeaders(Cell header, int index) { if (header == null || header.getCellType() != CellType.STRING) { - throw new FormatException(ErrorCode.XLSX_INVALID_HEADER_TYPE, - new Exception("Header at column " + index + " must be text, found: " + - (header == null ? "null" : header.getCellType()))); + String msg = "Header at column " + index + " must be text. Found: " + + (header == null ? "null" : header.getCellType()); + log.error("❌ {}", msg); + throw new FormatException(ErrorCode.XLSX_INVALID_HEADER_TYPE, new Exception(msg)); } + String headerString = header.getStringCellValue().trim(); if (headerString.isBlank()) { - throw new FormatException(ErrorCode.XLSX_NULL_HEADER, - new Exception("Header at column index " + index + " is blank")); + String msg = "Header at column index " + index + " is blank"; + log.error("❌ {}", msg); + throw new FormatException(ErrorCode.XLSX_NULL_HEADER, new Exception(msg)); } + if (columnOrder.contains(headerString)) { - throw new FormatException(ErrorCode.XLSX_DUPLICATE_HEADER, - new IllegalArgumentException("Duplicate header: '" + header + "' at index " + index)); + String msg = "Duplicate header: '" + headerString + "' at index " + index; + log.error("❌ {}", msg); + throw new FormatException(ErrorCode.XLSX_DUPLICATE_HEADER, new IllegalArgumentException(msg)); } } } diff --git a/src/main/java/org/unified/reporter/ReportCreator.java b/src/main/java/org/unified/reporter/ReportCreator.java deleted file mode 100644 index 2dee314..0000000 --- a/src/main/java/org/unified/reporter/ReportCreator.java +++ /dev/null @@ -1,164 +0,0 @@ -package org.unified.reporter; - -import net.sf.jasperreports.engine.*; -import net.sf.jasperreports.engine.data.JRMapCollectionDataSource; -import net.sf.jasperreports.engine.export.ooxml.JRXlsxExporter; -import net.sf.jasperreports.engine.util.JRLoader; -import net.sf.jasperreports.export.SimpleExporterInput; -import net.sf.jasperreports.export.SimpleOutputStreamExporterOutput; -import net.sf.jasperreports.export.SimpleXlsxReportConfiguration; -import org.unified.common.enums.ErrorCode; -import org.unified.common.enums.FileExportFormat; -import org.unified.common.exceptions.ReportException; - -import java.io.InputStream; -import java.nio.file.Path; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class ReportCreator { - - /** - * Generates a report from the provided data and JasperReports template, and exports it to the specified output path. - * - *

This method supports both compiled (.jasper) and uncompiled (.jrxml) Jasper templates. It fills the report with - * the given data and parameters, and exports it in the format specified by {@code format}.

- * - * @param data The list of data records to populate the report, where each map represents a row. - * @param additionalReportParameters Additional parameters to pass to the JasperReports engine (can be empty or null). - * @param outputPath The file system path (as a string) where the generated report should be saved. - * @param templateStream The input stream of the Jasper template (.jrxml or .jasper); must not be {@code null}. - * @param format The export format (e.g., PDF, HTML, XLSX); must not be {@code null}. - * @param isCompiledTemplate {@code true} if the template is precompiled (.jasper), {@code false} if raw (.jrxml). - * @throws ReportException if: - *
    - *
  • The {@code templateStream} is {@code null} ({@link ErrorCode#REPORT_TEMPLATE_NULL}).
  • - *
  • The {@code data} is {@code null} or empty ({@link ErrorCode#REPORT_DATA_EMPTY}).
  • - *
  • The {@code format} is {@code null} or unsupported ({@link ErrorCode#REPORT_FORMAT_UNSUPPORTED}).
  • - *
  • The report fails to be filled or exported ({@link ErrorCode#REPORT_FILL_FAILED} or {@link ErrorCode#REPORT_EXPORT_FAILED}).
  • - *
- */ - public static void generateReport( - List> data, - Map additionalReportParameters, - String outputPath, - InputStream templateStream, - FileExportFormat format, - boolean isCompiledTemplate // true if .jasper, false if .jrxml - ) { - if (templateStream == null) { - throw new ReportException(ErrorCode.REPORT_TEMPLATE_NULL); - } - - if (data == null || data.isEmpty()) { - throw new ReportException(ErrorCode.REPORT_DATA_EMPTY); - } - - try { - JasperReport report = getJasperReportFormat(templateStream, isCompiledTemplate); - JasperPrint print = fillReport(report, additionalReportParameters, data); - - exportReport(print, outputPath, format); - System.out.println(format + " report generated at: " + outputPath); - - } catch (JRException e) { - throw new ReportException(ErrorCode.REPORT_FILL_FAILED, e); - } catch (UnsupportedOperationException e) { - throw new ReportException(ErrorCode.REPORT_FORMAT_UNSUPPORTED, e); - } - } - - /** - * Overloaded convenience method for {@link #generateReport(List, Map, String, InputStream, FileExportFormat, boolean)} - * that accepts a {@link Path} for the output instead of a String. - * - * @param data The list of data records to populate the report. - * @param additionalReportParameters Additional parameters to pass to the JasperReports engine. - * @param outputPath The output file path as a {@link Path}; must not be {@code null}. - * @param templateStream The input stream of the Jasper template. - * @param format The export format (PDF, HTML, XLSX). - * @param isCompiledTemplate {@code true} if the template is a compiled .jasper file; otherwise, {@code false}. - * @throws ReportException if: - *
    - *
  • The {@code outputPath} is {@code null} ({@link ErrorCode#REPORT_OUTPUT_PATH_INVALID}).
  • - *
  • Any other error occurs in the main generation logic (see the main method).
  • - *
- */ - public static void generateReport( - List> data, - Map additionalReportParameters, - Path outputPath, - InputStream templateStream, - FileExportFormat format, - boolean isCompiledTemplate - ) { - if (outputPath == null) { - throw new ReportException(ErrorCode.REPORT_OUTPUT_PATH_INVALID); - } - generateReport(data, additionalReportParameters, outputPath.toString(), templateStream, format, isCompiledTemplate); - } - - private static JasperReport getJasperReportFormat(InputStream templateStream, boolean isCompiled) throws JRException { - try { - return isCompiled - ? (JasperReport) JRLoader.loadObject(templateStream) - : JasperCompileManager.compileReport(templateStream); - } catch (JRException e) { - throw new ReportException( - isCompiled ? ErrorCode.REPORT_TEMPLATE_LOAD_FAILED : ErrorCode.REPORT_TEMPLATE_COMPILE_FAILED, e - ); - } - } - - private static JasperPrint fillReport( - JasperReport report, - Map params, - List> data - ) throws JRException { - try { - // Defensive copy to make sure JasperReports can mutate safely - Map mutableParams = new HashMap<>(params); - - @SuppressWarnings("unchecked") - JRMapCollectionDataSource dataSource = - new JRMapCollectionDataSource((Collection>) (Collection) data); - - return JasperFillManager.fillReport(report, mutableParams, dataSource); - - } catch (JRException e) { - throw new ReportException(ErrorCode.REPORT_FILL_FAILED, e); - } - } - - private static void exportReport(JasperPrint print, String outputPath, FileExportFormat format) throws JRException { - if (format == null) { - throw new ReportException(ErrorCode.REPORT_FORMAT_UNSUPPORTED); - } - try { - switch (format) { - case PDF -> JasperExportManager.exportReportToPdfFile(print, outputPath); - case HTML -> JasperExportManager.exportReportToHtmlFile(print, outputPath); - case XLSX -> exportToXlsx(print, outputPath); - default -> throw new UnsupportedOperationException("Unsupported format: " + format); - } - } catch (JRException e) { - throw new ReportException(ErrorCode.REPORT_EXPORT_FAILED, e); - } - } - - private static void exportToXlsx(JasperPrint print, String outputPath) throws JRException { - JRXlsxExporter exporter = new JRXlsxExporter(); - exporter.setExporterInput(new SimpleExporterInput(print)); - exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(outputPath)); - - SimpleXlsxReportConfiguration config = new SimpleXlsxReportConfiguration(); - config.setOnePagePerSheet(false); - config.setDetectCellType(true); - config.setCollapseRowSpan(false); - - exporter.setConfiguration(config); - exporter.exportReport(); - } -} \ No newline at end of file diff --git a/src/main/java/org/unified/utils/ReportExporter.java b/src/main/java/org/unified/utils/ReportExporter.java new file mode 100644 index 0000000..42b5ce9 --- /dev/null +++ b/src/main/java/org/unified/utils/ReportExporter.java @@ -0,0 +1,151 @@ +package org.unified.utils; + +import lombok.extern.slf4j.Slf4j; +import net.sf.jasperreports.engine.*; +import net.sf.jasperreports.engine.data.JRMapCollectionDataSource; +import net.sf.jasperreports.engine.export.HtmlExporter; +import net.sf.jasperreports.engine.export.JRXmlExporter; +import net.sf.jasperreports.engine.export.ooxml.JRXlsxExporter; +import net.sf.jasperreports.export.*; +import org.unified.common.enums.ErrorCode; +import org.unified.common.enums.FileExportFormat; +import org.unified.common.exceptions.ReportException; + +import java.io.ByteArrayOutputStream; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * Utility class responsible for exporting reports using JasperReports into multiple formats. + *

+ * Supports export formats including PDF, HTML, XML, and XLSX. + */ +@Slf4j +public class ReportExporter { + + /** + * Exports the provided data and compiled JasperReport template to the specified format. + * + * @param dataRows the collection of data maps used as the data source for the report + * @param reportTemplate the compiled JasperReport (.jasper) + * @param parameters the map of report parameters + * @param format the output format (PDF, HTML, XML, XLSX) + * @return a byte array representing the exported report content + * @throws ReportException if any step of the export process fails + */ + public static byte[] export(Collection> dataRows, JasperReport reportTemplate, Map parameters, FileExportFormat format) { + if (dataRows == null || dataRows.isEmpty()) { + throw new ReportException(ErrorCode.REPORT_DATA_EMPTY); + } + try { + // Defensive copy of parameters + Map mutableParams = new HashMap<>(parameters); + + JasperPrint jasperPrint; + try { + @SuppressWarnings("unchecked") + JRMapCollectionDataSource dataSource = new JRMapCollectionDataSource( + (Collection>) (Collection) dataRows + ); + + jasperPrint = JasperFillManager.fillReport(reportTemplate, mutableParams, dataSource); + } catch (JRException e) { + log.error("❌ Failed to fill report with data", e); + throw new ReportException(ErrorCode.REPORT_FILL_FAILED, e); + } + + + return switch (format) { + case PDF -> exportToPdf(jasperPrint); + case HTML -> exportToHtml(jasperPrint); + case XML -> exportToXml(jasperPrint); + case XLSX -> exportToXlsx(jasperPrint); + default -> throw new ReportException(ErrorCode.REPORT_FORMAT_UNSUPPORTED); + }; + + } catch (ReportException e) { + throw e; + } catch (Exception e) { + log.error("❌ Unknown error during report export", e); + throw new ReportException(ErrorCode.UNKNOWN_ERROR, e); + } + } + + /** + * Exports the report to PDF format. + * + * @param jasperPrint the filled JasperPrint object + * @return the exported PDF as a byte array + */ + static byte[] exportToPdf(JasperPrint jasperPrint) { + try { + return JasperExportManager.exportReportToPdf(jasperPrint); + } catch (JRException e) { + throw new ReportException(ErrorCode.REPORT_EXPORT_FAILED, e); + } + } + + /** + * Exports the report to HTML format. + * + * @param jasperPrint the filled JasperPrint object + * @return the exported HTML as a byte array + */ + static byte[] exportToHtml(JasperPrint jasperPrint) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + HtmlExporter exporter = new HtmlExporter(); + exporter.setExporterInput(new SimpleExporterInput(jasperPrint)); + exporter.setExporterOutput(new SimpleHtmlExporterOutput(outputStream)); + exporter.exportReport(); + return outputStream.toByteArray(); + } catch (Exception e) { + throw new ReportException(ErrorCode.REPORT_EXPORT_FAILED, e); + } + } + + /** + * Exports the report to XML format. + * + * @param jasperPrint the filled JasperPrint object + * @return the exported XML as a byte array + */ + static byte[] exportToXml(JasperPrint jasperPrint) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + JRXmlExporter exporter = new JRXmlExporter(); + exporter.setExporterInput(new SimpleExporterInput(jasperPrint)); + exporter.setExporterOutput(new SimpleXmlExporterOutput(outputStream)); + exporter.exportReport(); + return outputStream.toByteArray(); + } catch (Exception e) { + throw new ReportException(ErrorCode.REPORT_EXPORT_FAILED, e); + } + } + + /** + * Exports the report to XLSX (Excel) format. + * + * @param jasperPrint the filled JasperPrint object + * @return the exported XLSX file as a byte array + */ + static byte[] exportToXlsx(JasperPrint jasperPrint) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + JRXlsxExporter exporter = new JRXlsxExporter(); + + exporter.setExporterInput(new SimpleExporterInput(jasperPrint)); + exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(outputStream)); + + SimpleXlsxReportConfiguration config = new SimpleXlsxReportConfiguration(); + config.setOnePagePerSheet(false); + config.setDetectCellType(true); + config.setCollapseRowSpan(false); + config.setWhitePageBackground(false); + exporter.setConfiguration(config); + + exporter.exportReport(); + return outputStream.toByteArray(); + } catch (Exception e) { + throw new ReportException(ErrorCode.REPORT_EXPORT_FAILED, e); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/unified/utils/ReportValidators.java b/src/main/java/org/unified/utils/ReportValidators.java new file mode 100644 index 0000000..bdd93dc --- /dev/null +++ b/src/main/java/org/unified/utils/ReportValidators.java @@ -0,0 +1,102 @@ +package org.unified.utils; + +import lombok.extern.slf4j.Slf4j; +import net.sf.jasperreports.engine.JRException; +import net.sf.jasperreports.engine.JasperCompileManager; +import net.sf.jasperreports.engine.JasperReport; +import net.sf.jasperreports.engine.util.JRLoader; +import org.unified.common.enums.ErrorCode; +import org.unified.common.exceptions.ReportException; +import org.unified.formats.UnifiedFormat; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Optional; + +/** + * Utility class that provides validations for Jasper report templates and UnifiedFormat inputs. + *

+ * Supports validation and loading of compiled JasperReports (.jasper) or compiling raw JRXML streams. + * Also ensures that input objects conform to expected formats before proceeding with reporting operations. + */ +@Slf4j +public class ReportValidators { + + /** + * Validates and loads a JasperReport from the provided input stream. + *

+ * Supports both compiled (.jasper) and source (.jrxml) templates. + * + * @param jasperReportTemplateStream the input stream of the report template + * @return a valid {@link JasperReport} instance + * @throws ReportException if the template is null, unreadable, or fails to load/compile + */ + public static JasperReport validateJasperReport(InputStream jasperReportTemplateStream) { + if (jasperReportTemplateStream == null) { + throw new ReportException(ErrorCode.REPORT_TEMPLATE_NULL); + } + + try { + byte[] templateBytes = jasperReportTemplateStream.readAllBytes(); + Optional reportTemplate = ReportValidators.loadTemplate(templateBytes); + + if (reportTemplate.isPresent()) { + log.info("✅ Valid Jasper template loaded: {}", reportTemplate.get().getName()); + return reportTemplate.get(); + } else { + throw new ReportException(ErrorCode.REPORT_TEMPLATE_LOAD_FAILED); + } + + } catch (IOException e) { + throw new ReportException(ErrorCode.IO_EXCEPTION, e); + } catch (Exception e) { + throw new ReportException(ErrorCode.REPORT_TEMPLATE_COMPILE_FAILED, e); + } + } + + /** + * Attempts to load a JasperReport from the given byte array. + * Tries first to deserialize a compiled .jasper file, and falls back to compiling a .jrxml source. + * + * @param data byte array of the Jasper template file + * @return Optional containing a valid {@link JasperReport}, or empty if loading/compilation failed + */ + private static Optional loadTemplate(byte[] data) { + try (InputStream is = new ByteArrayInputStream(data)) { + Object obj = JRLoader.loadObject(is); + if (obj instanceof JasperReport report) { + return Optional.of(report); + } + } catch (IOException | JRException ignored) { + } + + try (InputStream is = new ByteArrayInputStream(data)) { + return Optional.of(JasperCompileManager.compileReport(is)); + } catch (IOException | JRException ignored) { + } + + return Optional.empty(); + } + + /** + * Validates the provided input object for use in report generation. + *

+ * Accepts only instances of {@link UnifiedFormat}. Rejects InputStream or byte[] inputs + * explicitly to prevent misuse. + * + * @param input the input object to validate + * @return the input cast to {@link UnifiedFormat} if valid + * @throws ReportException if input type is invalid or unsupported + */ + public static UnifiedFormat validateInputFile(Object input) { + if (input instanceof UnifiedFormat) { + log.info("✅ Valid UnifiedFormat input received."); + return (UnifiedFormat) input; + } else if (input instanceof InputStream || input instanceof byte[]) { + throw new ReportException(ErrorCode.BYTE_UNSUPPORTED_FORMAT); + } else { + throw new ReportException(ErrorCode.UNKNOWN_ERROR, new IllegalArgumentException("Unrecognized input type")); + } + } +} diff --git a/src/test/java/org/unified/ReportGeneratorTest.java b/src/test/java/org/unified/ReportGeneratorTest.java new file mode 100644 index 0000000..431c980 --- /dev/null +++ b/src/test/java/org/unified/ReportGeneratorTest.java @@ -0,0 +1,143 @@ +package org.unified; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.unified.common.enums.ErrorCode; +import org.unified.common.enums.FileExportFormat; +import org.unified.common.exceptions.ReportException; +import org.unified.formats.UnifiedFormat; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class ReportGeneratorTest { + + static InputStream validJrxml; + static UnifiedFormat mockFormat; + + @BeforeAll + static void setup() { + // Load valid JRXML from test resources + validJrxml = ReportGeneratorTest.class.getResourceAsStream("/templates/simple_report.jrxml"); + assertNotNull(validJrxml, "JRXML template must exist in test resources"); + + // Mock UnifiedFormat with data + mockFormat = new UnifiedFormat() { + @Override + public List> getDataRows() { + Map row = new HashMap<>(); + row.put("name", "Alice"); + row.put("age", 30); + return List.of(row); + } + }; + } + + @Test + void generateReport_pdf_successfullyGeneratesReport() { + byte[] result = ReportGenerator.generateReport( + mockFormat, + validJrxml, + Map.of("ReportTitle", "Test"), + FileExportFormat.PDF + ); + + assertNotNull(result); + assertTrue(result.length > 0); + } + + @Test + void generateReport_withUnsupportedInputStream_throwsUnsupportedFormat() { + InputStream dummyStream = new ByteArrayInputStream(new byte[]{1, 2, 3}); + ReportException ex = assertThrows(ReportException.class, () -> + ReportGenerator.generateReport( + dummyStream, + validJrxml, + Map.of(), + FileExportFormat.XML + ) + ); + + assertEquals(ErrorCode.BYTE_UNSUPPORTED_FORMAT, ex.getErrorCode()); + } + + @Test + void generateReport_withNullTemplate_throwsTemplateNull() { + ReportException ex = assertThrows(ReportException.class, () -> + ReportGenerator.generateReport( + mockFormat, + null, + Map.of(), + FileExportFormat.XLSX + ) + ); + + assertEquals(ErrorCode.REPORT_TEMPLATE_NULL, ex.getErrorCode()); + } + + @Test + void generateReport_withInvalidTemplate_throwsTemplateLoadFailed() { + // Create a bogus stream (not .jrxml or .jasper) + InputStream invalidStream = new ByteArrayInputStream("not a jasper file".getBytes()); + + ReportException ex = assertThrows(ReportException.class, () -> + ReportGenerator.generateReport( + mockFormat, + invalidStream, + Map.of(), + FileExportFormat.PDF + ) + ); + + assertEquals(ErrorCode.REPORT_TEMPLATE_COMPILE_FAILED, ex.getErrorCode()); + } + + @Test + void generateReport_exportFails_throwsReportExportFailed() { + UnifiedFormat emptyFormat = new UnifiedFormat() { + @Override + public List> getDataRows() { + return Collections.emptyList(); // triggers export failure + } + }; + + ReportException ex = assertThrows(ReportException.class, () -> + ReportGenerator.generateReport( + emptyFormat, + ReportGeneratorTest.class.getResourceAsStream("/templates/simple_report.jrxml"), + Map.of(), + FileExportFormat.PDF + ) + ); + + assertEquals(ErrorCode.REPORT_DATA_EMPTY, ex.getErrorCode()); + } + + @Test + void generateReport_withUnexpectedError_throwsRuntimeException() { + // Use a UnifiedFormat that throws inside getDataRows() + UnifiedFormat brokenFormat = new UnifiedFormat() { + @Override + public List> getDataRows() { + throw new NullPointerException("Boom"); + } + }; + + RuntimeException ex = assertThrows(RuntimeException.class, () -> + ReportGenerator.generateReport( + brokenFormat, + validJrxml, + Map.of(), + FileExportFormat.HTML + ) + ); + + assertInstanceOf(ReportException.class, ex.getCause()); + } +} diff --git a/src/test/java/org/unified/formats/CSVFormatTest.java b/src/test/java/org/unified/formats/CSVFormatTest.java index 748780f..2ef75a4 100644 --- a/src/test/java/org/unified/formats/CSVFormatTest.java +++ b/src/test/java/org/unified/formats/CSVFormatTest.java @@ -90,4 +90,19 @@ void testSourceNameFallback() { CSVFormat parser = new CSVFormat(inputStream, null); assertEquals("CSV", parser.getSourceName()); } + + @Test + void testBlankRowsAreSkipped() { + InputStream inputStream = getClass().getResourceAsStream("/CSV/skip_blank_rows.csv"); + assertNotNull(inputStream, "Test CSV input stream must not be null"); + + CSVFormat parser = new CSVFormat(inputStream, "SkipBlank"); + + List> rows = parser.getDataRows(); + assertEquals(2, rows.size(), "Only two non-empty rows should be parsed"); + + assertEquals("John", rows.get(0).get("Name")); + assertEquals("Alice", rows.get(1).get("Name")); + } + } diff --git a/src/test/java/org/unified/formats/XLSXFormatTest.java b/src/test/java/org/unified/formats/XLSXFormatTest.java index 2b2dce2..db7fda7 100644 --- a/src/test/java/org/unified/formats/XLSXFormatTest.java +++ b/src/test/java/org/unified/formats/XLSXFormatTest.java @@ -1,16 +1,103 @@ package org.unified.formats; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.junit.jupiter.api.Test; import org.unified.common.enums.ErrorCode; import org.unified.common.exceptions.FormatException; import java.io.InputStream; +import java.util.Date; import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.*; class XLSXFormatTest { + InputStream is = getClass().getResourceAsStream("/XLSX/valid.xlsx"); + private final XLSXFormat format = new XLSXFormat(is, "Test"); + + + @Test + void testStringCellValue() { + Cell cell = createCell(CellType.STRING); + cell.setCellValue("Hello"); + assertEquals("Hello", format.getCellValue(cell)); + } + + @Test + void testBooleanCellValue() { + Cell cell = createCell(CellType.BOOLEAN); + cell.setCellValue(true); + assertEquals(true, format.getCellValue(cell)); + } + + @Test + void testNumericCellValue() { + Cell cell = createCell(CellType.NUMERIC); + cell.setCellValue(42.0); + assertEquals(42.0, format.getCellValue(cell)); + } + + @Test + void testDateCellValue() { + Cell cell = createCell(CellType.NUMERIC); + Date date = new Date(); + cell.setCellValue(date); + CellStyle style = cell.getSheet().getWorkbook().createCellStyle(); + style.setDataFormat((short) 14); // e.g., "m/d/yy" + cell.setCellStyle(style); + + DataFormat df = cell.getSheet().getWorkbook().createDataFormat(); + style.setDataFormat(df.getFormat("m/d/yy")); + + assertEquals(date, format.getCellValue(cell)); + } + + @Test + void testFormulaCellValue() { + Cell cell = createCell(CellType.FORMULA); + cell.setCellFormula("SUM(A1:A2)"); + assertEquals("SUM(A1:A2)", format.getCellValue(cell)); + } + + @Test + void testBlankCellValue() { + Cell cell = createCell(CellType.BLANK); + assertNull(format.getCellValue(cell)); + } + + @Test + void testErrorCellValue() { + Cell cell = createCell(CellType.ERROR); + assertNull(format.getCellValue(cell)); + } + + @Test + void testNoneCellTypeReturnsNull() { + Workbook workbook = new XSSFWorkbook(); + Sheet sheet = workbook.createSheet("Sheet1"); + Row row = sheet.createRow(0); + Cell cell = row.createCell(0); + + assertEquals(CellType.BLANK, cell.getCellType()); + + assertNull(format.getCellValue(cell)); + } + + + @Test + void testNullCellReturnsNull() { + assertNull(format.getCellValue(null)); + } + + // Helper method to create a cell of given type + private Cell createCell(CellType type) { + Workbook workbook = new XSSFWorkbook(); + Sheet sheet = workbook.createSheet("Sheet1"); + Row row = sheet.createRow(0); + return row.createCell(0, type); + } @Test void testValidXlsxParsing() { diff --git a/src/test/java/org/unified/reporter/ReportCreatorTest.java b/src/test/java/org/unified/reporter/ReportCreatorTest.java deleted file mode 100644 index eacef7d..0000000 --- a/src/test/java/org/unified/reporter/ReportCreatorTest.java +++ /dev/null @@ -1,131 +0,0 @@ -package org.unified.reporter; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.unified.common.enums.ErrorCode; -import org.unified.common.enums.FileExportFormat; -import org.unified.common.exceptions.ReportException; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Comparator; -import java.util.List; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.*; - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class ReportCreatorTest { - - private List> sampleData; - private Map params; - private Path outputDir; - - @BeforeAll - void setup() throws IOException { - sampleData = List.of( - Map.of("name", "Alice", "score", 85), - Map.of("name", "Bob", "score", 92) - ); - params = Map.of("ReportTitle", "Test Report"); - - outputDir = Files.createTempDirectory("report-tests"); - } - - @AfterAll - void cleanup() throws IOException { - Files.walk(outputDir) - .sorted(Comparator.reverseOrder()) - .forEach(path -> { - try { - Files.deleteIfExists(path); - } catch (IOException ignored) { - } - }); - } - - @Test - void shouldGeneratePdfFromJrxmlTemplate() throws Exception { - try (InputStream jrxml = getClass().getResourceAsStream("/templates/simple_report.jrxml")) { - Path output = outputDir.resolve("report.pdf"); - - ReportCreator.generateReport(sampleData, params, output, jrxml, FileExportFormat.PDF, false); - - assertTrue(Files.exists(output), "PDF report should be generated"); - } - } - - @Test - void shouldGenerateXlsxFromCompiledTemplate() throws Exception { - try (InputStream jasper = getClass().getResourceAsStream("/templates/simple_report.jasper")) { - Path output = outputDir.resolve("report.xlsx"); - - ReportCreator.generateReport(sampleData, params, output, jasper, FileExportFormat.XLSX, true); - - assertTrue(Files.exists(output), "XLSX report should be generated"); - } - } - - @Test - void shouldThrowIfTemplateStreamIsNull() { - ReportException ex = assertThrows(ReportException.class, () -> - ReportCreator.generateReport(sampleData, params, "invalid", null, FileExportFormat.PDF, false) - ); - assertEquals(ErrorCode.REPORT_TEMPLATE_NULL, ex.getErrorCode()); - } - - @Test - void shouldThrowIfDataIsEmpty() { - try (InputStream jrxml = getClass().getResourceAsStream("/templates/simple_report.jrxml")) { - ReportException ex = assertThrows(ReportException.class, () -> - ReportCreator.generateReport(List.of(), params, "invalid", jrxml, FileExportFormat.PDF, false) - ); - assertEquals(ErrorCode.REPORT_DATA_EMPTY, ex.getErrorCode()); - } catch (IOException e) { - fail("Template loading failed"); - } - } - - @Test - void shouldThrowForUnsupportedFormat() { - try (InputStream jrxml = getClass().getResourceAsStream("/templates/simple_report.jrxml")) { - ReportException ex = assertThrows(ReportException.class, () -> - ReportCreator.generateReport(sampleData, params, "invalid", jrxml, null, false) - ); - assertEquals(ErrorCode.REPORT_FORMAT_UNSUPPORTED, ex.getErrorCode()); - } catch (IOException e) { - fail("Template loading failed"); - } - } - - @Test - void shouldThrowIfOutputPathIsNull() { - try (InputStream jrxml = getClass().getResourceAsStream("/templates/simple_report.jrxml")) { - ReportException ex = assertThrows(ReportException.class, () -> - ReportCreator.generateReport(sampleData, params, (Path) null, jrxml, FileExportFormat.PDF, false) - ); - assertEquals(ErrorCode.REPORT_OUTPUT_PATH_INVALID, ex.getErrorCode()); - } catch (IOException e) { - fail("Template loading failed"); - } - } - -// @Test -// void compileJrxmlTemplateToJasper() throws Exception { -// try (InputStream jrxml = getClass().getResourceAsStream("/templates/simple_report.jrxml")) { -// JasperReport report = JasperCompileManager.compileReport(jrxml); -// -// // Save the compiled file -// JasperCompileManager.compileReportToFile( -// getClass().getResource("/templates/simple_report.jrxml").getPath(), -// "src/test/resources/templates/simple_report.jasper" -// ); -// -// assertNotNull(report); -// } -// } -} diff --git a/src/test/java/org/unified/utils/ReportExporterTest.java b/src/test/java/org/unified/utils/ReportExporterTest.java new file mode 100644 index 0000000..f2f7406 --- /dev/null +++ b/src/test/java/org/unified/utils/ReportExporterTest.java @@ -0,0 +1,110 @@ +package org.unified.utils; + +import net.sf.jasperreports.engine.JasperCompileManager; +import net.sf.jasperreports.engine.JasperPrint; +import net.sf.jasperreports.engine.JasperReport; +import net.sf.jasperreports.engine.design.JasperDesign; +import net.sf.jasperreports.engine.xml.JRXmlLoader; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.unified.common.enums.ErrorCode; +import org.unified.common.enums.FileExportFormat; +import org.unified.common.exceptions.ReportException; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; + +class ReportExporterTest { + + private static JasperReport report; + + @BeforeAll + static void setup() throws Exception { + try (InputStream is = ReportExporterTest.class.getResourceAsStream("/templates/simple_report.jrxml")) { + assertNotNull(is, "sample_report.jrxml must be in test resources"); + JasperDesign design = JRXmlLoader.load(is); + report = JasperCompileManager.compileReport(design); + } + } + + private List> sampleData() { + Map row = new HashMap<>(); + row.put("name", "Alice"); + row.put("age", 30); + return List.of(row); + } + + private Map sampleParams() { + Map params = new HashMap<>(); + params.put("ReportTitle", "Test Report"); + return params; + } + + @Test + void exportToPdf_returnsNonEmptyByteArray() { + byte[] bytes = ReportExporter.export(sampleData(), report, sampleParams(), FileExportFormat.PDF); + assertNotNull(bytes); + assertTrue(bytes.length > 0); + } + + @Test + void exportToHtml_returnsNonEmptyByteArray() { + byte[] bytes = ReportExporter.export(sampleData(), report, sampleParams(), FileExportFormat.HTML); + assertNotNull(bytes); + assertTrue(bytes.length > 0); + } + + @Test + void exportToXml_returnsNonEmptyByteArray() { + byte[] bytes = ReportExporter.export(sampleData(), report, sampleParams(), FileExportFormat.XML); + assertNotNull(bytes); + assertTrue(bytes.length > 0); + } + + @Test + void exportToXlsx_returnsNonEmptyByteArray() { + byte[] bytes = ReportExporter.export(sampleData(), report, sampleParams(), FileExportFormat.XLSX); + assertNotNull(bytes); + assertTrue(bytes.length > 0); + } + + @Test + void export_withEmptyData_throwsException() { + List> emptyData = new ArrayList<>(); + ReportException ex = assertThrows(ReportException.class, () -> + ReportExporter.export(emptyData, report, sampleParams(), FileExportFormat.PDF)); + assertEquals(ErrorCode.REPORT_DATA_EMPTY, ex.getErrorCode()); + } + + @Test + void export_withNullData_throwsException() { + ReportException ex = assertThrows(ReportException.class, () -> + ReportExporter.export(null, report, sampleParams(), FileExportFormat.PDF)); + assertEquals(ErrorCode.REPORT_DATA_EMPTY, ex.getErrorCode()); + } + + @Test + void exportToXlsx_whenExporterFails_throwsExportFailedException() { + JasperPrint mockPrint = mock(JasperPrint.class); + List> data = sampleData(); + ReportException ex = assertThrows(ReportException.class, () -> { + ReportExporter.exportToXlsx(mockPrint); + }); + assertEquals(ErrorCode.REPORT_EXPORT_FAILED, ex.getErrorCode()); + } + + @Test + void export_withUnsupportedFormat_throwsException() { + FileExportFormat unsupported = FileExportFormat.DOCX; + ReportException ex = assertThrows(ReportException.class, () -> + ReportExporter.export(sampleData(), report, sampleParams(), unsupported)); + assertEquals(ErrorCode.REPORT_FORMAT_UNSUPPORTED, ex.getErrorCode()); + } + +} diff --git a/src/test/java/org/unified/utils/ReportValidatorsTest.java b/src/test/java/org/unified/utils/ReportValidatorsTest.java new file mode 100644 index 0000000..c8179c7 --- /dev/null +++ b/src/test/java/org/unified/utils/ReportValidatorsTest.java @@ -0,0 +1,115 @@ +package org.unified.utils; + +import net.sf.jasperreports.engine.JasperReport; +import org.junit.jupiter.api.Test; +import org.unified.common.enums.ErrorCode; +import org.unified.common.exceptions.ReportException; +import org.unified.formats.UnifiedFormat; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class ReportValidatorsTest { + + @Test + void validateJasperReport_withNullInput_throwsTemplateNullException() { + ReportException ex = assertThrows(ReportException.class, () -> + ReportValidators.validateJasperReport(null)); + assertEquals(ErrorCode.REPORT_TEMPLATE_NULL, ex.getErrorCode()); + } + + @Test + void validateJasperReport_withInvalidBytes_throwsLoadFailedException() { + byte[] invalidData = "not-a-valid-template".getBytes(); + InputStream stream = new ByteArrayInputStream(invalidData); + + ReportException ex = assertThrows(ReportException.class, () -> + ReportValidators.validateJasperReport(stream)); + assertEquals(ErrorCode.REPORT_TEMPLATE_COMPILE_FAILED, ex.getErrorCode()); + } + + @Test + void validateJasperReport_withValidJrxml_compilesSuccessfully() throws Exception { + try (InputStream jrxml = getClass().getResourceAsStream("/templates/simple_report.jrxml")) { + assertNotNull(jrxml, "JRXML template must be available in test resources"); + JasperReport report = ReportValidators.validateJasperReport(jrxml); + assertNotNull(report); + assertNotNull(report.getName()); + } + } + + @Test + void validateJasperReport_withValidJasper_compilesSuccessfully() throws Exception { + try (InputStream jrxml = getClass().getResourceAsStream("/templates/simple_report.jasper")) { + assertNotNull(jrxml, "Jasper template must be available in test resources"); + JasperReport report = ReportValidators.validateJasperReport(jrxml); + assertNotNull(report); + assertNotNull(report.getName()); + } + } + + @Test + void validateInputFile_withValidUnifiedFormat_passesValidation() { + UnifiedFormat input = new UnifiedFormat() { + @Override + public List> getDataRows() { + return List.of(); + } + // implement interface or use mockito if available + }; + + UnifiedFormat validated = ReportValidators.validateInputFile(input); + assertSame(input, validated); + } + + @Test + void validateInputFile_withInputStream_throwsUnsupportedFormat() { + InputStream dummyStream = new ByteArrayInputStream(new byte[]{}); + ReportException ex = assertThrows(ReportException.class, () -> + ReportValidators.validateInputFile(dummyStream)); + assertEquals(ErrorCode.BYTE_UNSUPPORTED_FORMAT, ex.getErrorCode()); + } + + @Test + void validateInputFile_withByteArray_throwsUnsupportedFormat() { + byte[] data = new byte[]{1, 2, 3}; + ReportException ex = assertThrows(ReportException.class, () -> + ReportValidators.validateInputFile(data)); + assertEquals(ErrorCode.BYTE_UNSUPPORTED_FORMAT, ex.getErrorCode()); + } + + @Test + void validateInputFile_withUnsupportedType_throwsUnknownError() { + Object randomInput = 12345; // e.g., Integer + ReportException ex = assertThrows(ReportException.class, () -> + ReportValidators.validateInputFile(randomInput)); + assertEquals(ErrorCode.UNKNOWN_ERROR, ex.getErrorCode()); + assertTrue(ex.getCause() instanceof IllegalArgumentException); + } + + @Test + void validateJasperReport_whenIOExceptionThrown_throwsIoExceptionErrorCode() { + InputStream brokenStream = new InputStream() { + @Override + public int read() throws IOException { + throw new IOException("Simulated read failure"); + } + + @Override + public byte[] readAllBytes() throws IOException { + throw new IOException("Simulated readAllBytes failure"); + } + }; + + ReportException ex = assertThrows(ReportException.class, () -> + ReportValidators.validateJasperReport(brokenStream)); + + assertEquals(ErrorCode.IO_EXCEPTION, ex.getErrorCode()); + assertTrue(ex.getCause() instanceof IOException); + } +} diff --git a/src/test/resources/CSV/skip_blank_rows.csv b/src/test/resources/CSV/skip_blank_rows.csv new file mode 100644 index 0000000..40883a7 --- /dev/null +++ b/src/test/resources/CSV/skip_blank_rows.csv @@ -0,0 +1,5 @@ +Name,Age +John,30 + + +Alice,25 \ No newline at end of file diff --git a/src/test/resources/log4j2-test.xml b/src/test/resources/log4j2-test.xml new file mode 100644 index 0000000..b09ea75 --- /dev/null +++ b/src/test/resources/log4j2-test.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + diff --git a/src/test/resources/templates/simple_report.jrxml b/src/test/resources/templates/simple_report.jrxml index bfdad16..8c57bca 100644 --- a/src/test/resources/templates/simple_report.jrxml +++ b/src/test/resources/templates/simple_report.jrxml @@ -1,7 +1,7 @@