diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml new file mode 100644 index 0000000..4f6145b --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,44 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened, synchronize, ready_for_review, reopened] + # Optional: Only run on specific file changes + # paths: + # - "src/**/*.ts" + # - "src/**/*.tsx" + # - "src/**/*.js" + # - "src/**/*.jsx" + +jobs: + claude-review: + # Optional: Filter by PR author + # if: | + # github.event.pull_request.user.login == 'external-contributor' || + # github.event.pull_request.user.login == 'new-developer' || + # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' + + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code Review + id: claude-review + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' + plugins: 'code-review@claude-code-plugins' + prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 0000000..79fe056 --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,50 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + actions: read # Required for Claude to read CI results on PRs + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + + # This is an optional setting that allows Claude to read CI results on PRs + additional_permissions: | + actions: read + + # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. + # prompt: 'Update the pull request description to include a summary of changes.' + + # Optional: Add claude_args to customize behavior and configuration + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + # claude_args: '--allowed-tools Bash(gh pr:*)' + diff --git a/build.gradle b/build.gradle index 1231158..9d33db7 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,7 @@ distributions { dependencies { implementation("org.apache.poi:poi-ooxml:3.12") implementation("org.apache.odftoolkit:simple-odf:0.8.2-incubating") + implementation("org.apache.commons:commons-io:1.3.2") implementation("com.google.code.findbugs:jsr305:3.0.2") } diff --git a/src/main/java/com/ka/spreadsheet/diff/Flags.java b/src/main/java/com/ka/spreadsheet/diff/Flags.java index ed33fa7..5e1bd65 100644 --- a/src/main/java/com/ka/spreadsheet/diff/Flags.java +++ b/src/main/java/com/ka/spreadsheet/diff/Flags.java @@ -15,6 +15,7 @@ public class Flags { public static Double DIFF_NUMERIC_PRECISION; public static boolean DIFF_IGNORE_FORMULAS; public static DiffFormatter DIFF_FORMAT; + public static String LOG_FILENAME; public static File WORKBOOK1; public static File WORKBOOK2; public static WorkbookIgnores WORKBOOK_IGNORES1; @@ -22,7 +23,9 @@ public class Flags { public enum DiffFormatter { EXCEL_CMP, - UNIFIED + UNIFIED, + LOGGER, + LOGGER_UNIFIED } public static boolean parseFlags(String[] args) { @@ -58,8 +61,9 @@ public static boolean parseFlags(String[] args) { System.out.println(usage()); return false; } - WORKBOOK1 = new File(args[0]); - WORKBOOK2 = new File(args[1]); + LOG_FILENAME = args[0]; + WORKBOOK1 = new File(args[1]); + WORKBOOK2 = new File(args[2]); WORKBOOK_IGNORES1 = WorkbookIgnores.parseWorkbookIgnores(args, "--ignore1"); WORKBOOK_IGNORES2 = WorkbookIgnores.parseWorkbookIgnores(args, "--ignore2"); return true; diff --git a/src/main/java/com/ka/spreadsheet/diff/LoggerSpreadSheetDiffCallback.java b/src/main/java/com/ka/spreadsheet/diff/LoggerSpreadSheetDiffCallback.java new file mode 100644 index 0000000..f1491d8 --- /dev/null +++ b/src/main/java/com/ka/spreadsheet/diff/LoggerSpreadSheetDiffCallback.java @@ -0,0 +1,97 @@ +package com.ka.spreadsheet.diff; + + +import org.apache.log4j.LogManager; +import org.apache.log4j.Logger; + +import java.util.LinkedHashSet; +import java.util.Set; + +public class LoggerSpreadSheetDiffCallback extends SpreadSheetDiffCallbackBase { + private static final Logger cleanLogger = LogManager.getLogger("DisplayLogger"); + private final Set sheets = new LinkedHashSet(); + private final Set rows = new LinkedHashSet(); + private final Set cols = new LinkedHashSet(); + private final Set macros = new LinkedHashSet(); + + private final Set sheets1 = new LinkedHashSet(); + private final Set rows1 = new LinkedHashSet(); + private final Set cols1 = new LinkedHashSet(); + + private final Set sheets2 = new LinkedHashSet(); + private final Set rows2 = new LinkedHashSet(); + private final Set cols2 = new LinkedHashSet(); + + private final Set macros1 = new LinkedHashSet(); + private final Set macros2 = new LinkedHashSet(); + + private String file1; + private String file2; + + @Override + public void init(String file1, String file2) { + super.init(file1, file2); + this.file1 = file1; + this.file2 = file2; + } + + @Override + public void reportWorkbooksDiffer(boolean differ) { + super.reportWorkbooksDiffer(differ); + reportSummary("DIFF", sheets, rows, cols, macros); + reportSummary("EXTRA WB1", sheets1, rows1, cols1, macros1); + reportSummary("EXTRA WB2", sheets2, rows2, cols2, macros2); + cleanLogger.info("-----------------------------------------"); + cleanLogger.info("Excel files " + file1 + " and " + file2 + " " + + (differ ? "differ" : "match")); + } + + @Override + public void reportMacroOnlyIn(boolean inFirstSpreadSheet) { + super.reportMacroOnlyIn(inFirstSpreadSheet); + String name = "unknown"; + (inFirstSpreadSheet ? macros1 : macros2).add(name); + cleanLogger.info("EXTRA macro name: " + name + " found only in " + wb(inFirstSpreadSheet)); + } + + @Override + public void reportExtraCell(boolean inFirstSpreadSheet, CellPos c) { + super.reportExtraCell(inFirstSpreadSheet, c); + if (inFirstSpreadSheet) { + sheets1.add(c.getSheetName()); + rows1.add(c.getRow()); + cols1.add(c.getColumn()); + } else { + sheets2.add(c.getSheetName()); + rows2.add(c.getRow()); + cols2.add(c.getColumn()); + } + cleanLogger.info("EXTRA Cell in " + wb(inFirstSpreadSheet) + " " + c.getCellPosition() + + " => '" + c.getCellValue() + "'"); + } + + @Override + public void reportDiffCell(CellPos c1, CellPos c2) { + super.reportDiffCell(c1, c2); + sheets.add(c1.getSheetName()); + rows.add(c1.getRow()); + cols.add(c1.getColumn()); + cleanLogger.info("DIFF Cell at " + c1.getCellPosition() + " => '" + c1.getCellValue() + + "' v/s '" + c2.getCellValue() + "'"); + } + + private void reportSummary(String what, Set sheets, Set rows, Set cols, + Set macros) { + cleanLogger.info("----------------- " + what + " -------------------"); + cleanLogger.info("Sheets: " + sheets); + cleanLogger.info("Rows: " + rows); + cleanLogger.info("Cols: " + cols); + if (!macros.isEmpty()) { + cleanLogger.info("Macros: " + macros); + } + } + + private String wb(boolean inFirstSpreadSheet) { + return inFirstSpreadSheet ? "WB1" : "WB2"; + } +} diff --git a/src/main/java/com/ka/spreadsheet/diff/LoggerUnifiedDiffSpreadSheetDiffCallback.java b/src/main/java/com/ka/spreadsheet/diff/LoggerUnifiedDiffSpreadSheetDiffCallback.java new file mode 100644 index 0000000..b3486cf --- /dev/null +++ b/src/main/java/com/ka/spreadsheet/diff/LoggerUnifiedDiffSpreadSheetDiffCallback.java @@ -0,0 +1,156 @@ +package com.ka.spreadsheet.diff; + + +import org.apache.log4j.LogManager; +import org.apache.log4j.Logger; + +import java.util.ArrayList; +import java.util.List; + +import static com.ka.spreadsheet.diff.SpreadSheetUtils.CELL_INTERNAL_TO_USER; + +// Output format, inspired by the traditional Unified Diff format (but using cell references instead of line numbers): +// +--- column 1 +// V +// --- file_1_name!sheet_1_name +// +++ file_2_name!sheet_1_name +// @@ cell_1_ref,cell_n_ref cell_1_ref,cell_n_ref @@ +// -old_cell_1_data +// -... +// -old_cell_n_data +// +new_cell_1_data +// +... +// +new_cell_n_data +// @@@ ... +// ... +// --- file_1_name!sheet_2_name +// +++ file_2_name!sheet_2_name +// ... +// Each cell data line is always present, even if the data is empty (thus just "-" or "+). +public class LoggerUnifiedDiffSpreadSheetDiffCallback extends SpreadSheetDiffCallbackBase { + private static final Logger cleanLogger = LogManager.getLogger("DisplayLogger"); + private final String lineSeparator = System.getProperty("line.separator"); + private String file1; + private String file2; + private DiffCell prevDiffCell = new DiffCell("", -2, -2, null, null); + private List currentCellBlock = new ArrayList(); + + @Override + public void init(String file1, String file2) { + super.init(file1, file2); + this.file1 = file1; + this.file2 = file2; + } + + @Override + public void finish() { + super.finish(); + printAndEmptyCellBlock(); + } + + @Override + public void reportMacroOnlyIn(boolean inFirstSpreadSheet) { + super.reportMacroOnlyIn(inFirstSpreadSheet); + cleanLogger.info("Unified diff format does not support macros, however WB" + (inFirstSpreadSheet ? "1" : "2") + " contains at least one macro that is not in the other workbook."); + } + + @Override + public void reportExtraCell(boolean inFirstSpreadSheet, CellPos c) { + super.reportExtraCell(inFirstSpreadSheet, c); + accumulateAndMaybePrint(new DiffCell( + c.getSheetName(), + c.getRowIndex(), + c.getColumnIndex(), + (inFirstSpreadSheet ? c.getCellValue() : null), + (!inFirstSpreadSheet ? c.getCellValue() : null) + )); + } + + @Override + public void reportDiffCell(CellPos c1, CellPos c2) { + super.reportDiffCell(c1, c2); + accumulateAndMaybePrint(new DiffCell( + c1.getSheetName(), + c1.getRowIndex(), + c1.getColumnIndex(), + c1.getCellValue(), + c2.getCellValue() + )); + } + + private void accumulateAndMaybePrint(DiffCell diffCell) { + if (!isSameSheet(prevDiffCell, diffCell)) { + printAndEmptyCellBlock(); + cleanLogger.info("--- " + file1 + "!" + diffCell.sheetName); + cleanLogger.info("+++ " + file2 + "!" + diffCell.sheetName); + } + if (!isSameRow(prevDiffCell, diffCell)) { + printAndEmptyCellBlock(); + } + if (!isSameCellBlock(prevDiffCell, diffCell)) { + printAndEmptyCellBlock(); + } + currentCellBlock.add(diffCell); + prevDiffCell = diffCell; + } + + private boolean isSameSheet(DiffCell c1, DiffCell c2) { + return c1.sheetName.equals(c2.sheetName); + } + + private boolean isSameRow(DiffCell c1, DiffCell c2) { + return c1.rowIndex == c2.rowIndex; + } + + private boolean isSameCellBlock(DiffCell c1, DiffCell c2) { + return c1.colIndex == (c2.colIndex - 1); + } + + // TODO: Make this handle multiple rows, maybe. What would that output look like? This might be a bad idea. + private void printAndEmptyCellBlock() { + if (currentCellBlock.size() > 0) { + StringBuilder sheet1Lines = new StringBuilder(); + StringBuilder sheet2Lines = new StringBuilder(); + String cellRange = CELL_INTERNAL_TO_USER(currentCellBlock.get(0).rowIndex, currentCellBlock.get(0).colIndex); + if (currentCellBlock.size() > 1) { + cellRange = cellRange + "," + + CELL_INTERNAL_TO_USER(currentCellBlock.get(0).rowIndex, currentCellBlock.get(currentCellBlock.size()-1).colIndex); + } + cleanLogger.info("@@ -" + cellRange + " +" + cellRange + " @@"); + int prevCol = -1; + for (DiffCell rowCell : currentCellBlock) { + assert currentCellBlock.get(0).rowIndex == rowCell.rowIndex : "printAndEmptyCellBlock() only supports one row at a time."; + assert prevCol == -1 || prevCol == (rowCell.colIndex -1 ) : "printAndEmptyCellBlock() only supports a contiguous range of cells at a time."; + sheet1Lines.append("-"); + sheet2Lines.append("+"); + if (rowCell.c1Value != null) { + sheet1Lines.append(rowCell.c1Value); + } + if (rowCell.c2Value != null) { + sheet2Lines.append(rowCell.c2Value); + } + sheet1Lines.append(lineSeparator); + sheet2Lines.append(lineSeparator); + } + cleanLogger.info(sheet1Lines.toString()); + cleanLogger.info(sheet2Lines.toString()); + } + currentCellBlock = new ArrayList(); + } + + private class DiffCell { + String sheetName; + int rowIndex; + int colIndex; + CellValue c1Value; + CellValue c2Value; + + public DiffCell(String _sheetName, int _rowIndex, int _colIndex, CellValue _c1Value, CellValue _c2Value) { + sheetName = _sheetName; + rowIndex = _rowIndex; + colIndex = _colIndex; + c1Value = _c1Value; + c2Value = _c2Value; + } + } +} diff --git a/src/main/java/com/ka/spreadsheet/diff/SpreadSheetDiffer.java b/src/main/java/com/ka/spreadsheet/diff/SpreadSheetDiffer.java index 4a870e4..e729608 100644 --- a/src/main/java/com/ka/spreadsheet/diff/SpreadSheetDiffer.java +++ b/src/main/java/com/ka/spreadsheet/diff/SpreadSheetDiffer.java @@ -1,18 +1,22 @@ package com.ka.spreadsheet.diff; +import com.ka.spreadsheet.util.LogUtil; + import static com.ka.spreadsheet.diff.Flags.WORKBOOK1; import static com.ka.spreadsheet.diff.Flags.WORKBOOK2; import java.io.File; import java.util.Iterator; +import org.apache.log4j.LogManager; +import org.apache.log4j.Logger; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.usermodel.WorkbookFactory; import org.odftoolkit.simple.SpreadsheetDocument; public class SpreadSheetDiffer { - + private static final Logger logger = LogManager.getLogger(SpreadSheetDiffer.class); public static void main(String[] args) { int ret = doDiff(args); System.exit(ret); @@ -22,6 +26,9 @@ public static int doDiff(String[] args) { int ret = -1; try { if (Flags.parseFlags(args)) { + LogUtil.initLogging(Flags.LOG_FILENAME); + LogUtil.initCleanLogging("display" + Flags.LOG_FILENAME); + logger.info("Hello"); SpreadSheetDiffCallback formatter; switch (Flags.DIFF_FORMAT) { case EXCEL_CMP: @@ -30,6 +37,12 @@ public static int doDiff(String[] args) { case UNIFIED: formatter = new UnifiedDiffSpreadSheetDiffCallback(); break; + case LOGGER: + formatter = new LoggerSpreadSheetDiffCallback(); + break; + case LOGGER_UNIFIED: + formatter = new LoggerUnifiedDiffSpreadSheetDiffCallback(); + break; default: throw new IllegalArgumentException("Unknown diff formatter"); } @@ -42,7 +55,7 @@ public static int doDiff(String[] args) { System.err.println("Diff failed: " + e.getMessage()); } } - return ret; + return 0; } public static int doDiff(SpreadSheetDiffCallback diffCallback) throws Exception { diff --git a/src/main/java/com/ka/spreadsheet/diff/UnifiedDiffSpreadSheetDiffCallback.java b/src/main/java/com/ka/spreadsheet/diff/UnifiedDiffSpreadSheetDiffCallback.java index 3a00e1a..3d283e8 100644 --- a/src/main/java/com/ka/spreadsheet/diff/UnifiedDiffSpreadSheetDiffCallback.java +++ b/src/main/java/com/ka/spreadsheet/diff/UnifiedDiffSpreadSheetDiffCallback.java @@ -1,6 +1,6 @@ package com.ka.spreadsheet.diff; -import java.io.File; + import java.util.ArrayList; import java.util.List; @@ -25,7 +25,6 @@ // ... // Each cell data line is always present, even if the data is empty (thus just "-" or "+). public class UnifiedDiffSpreadSheetDiffCallback extends SpreadSheetDiffCallbackBase { - private final String lineSeparator = System.getProperty("line.separator"); private String file1; private String file2; diff --git a/src/main/java/com/ka/spreadsheet/sort/ColumnSort.java b/src/main/java/com/ka/spreadsheet/sort/ColumnSort.java new file mode 100644 index 0000000..ae0388c --- /dev/null +++ b/src/main/java/com/ka/spreadsheet/sort/ColumnSort.java @@ -0,0 +1,22 @@ +package com.ka.spreadsheet.sort; + +public class ColumnSort { + public enum SortDirection{ + Ascending,Descending + } + private int columnIndex; + private SortDirection sortDirection; + + public ColumnSort(int columnIndex,SortDirection sortDirection){ + this.columnIndex=columnIndex; + this.sortDirection=sortDirection; + } + + public int getColumnIndex() { + return columnIndex; + } + + public SortDirection getSortDirection() { + return sortDirection; + } +} diff --git a/src/main/java/com/ka/spreadsheet/sort/ExcelSorter.java b/src/main/java/com/ka/spreadsheet/sort/ExcelSorter.java new file mode 100644 index 0000000..0a5fb42 --- /dev/null +++ b/src/main/java/com/ka/spreadsheet/sort/ExcelSorter.java @@ -0,0 +1,308 @@ +package com.ka.spreadsheet.sort; + +import com.ka.spreadsheet.util.LogUtil; +import org.apache.log4j.Logger; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.xssf.usermodel.*; +import org.apache.commons.io.FilenameUtils; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.List; + + +public class ExcelSorter { + private static Logger log = Logger.getLogger(ExcelSorter.class); + private static String WORKBOOK_FILENAME; + private static String LOG_FILENAME; + private static final String instructions = "FilePath: The fully qualified path to the Excel workbook file you want to sort.\n" + + "LogFile: The name of the log file to use for the application.\n"+ + "SheetSort: A formatted value that indicates the sheet to be sorted, the columns to sort by and the starting row.\n" + + " Each sheet sort argument has the following format:\n"+ + " [Sheet Index:{Column Index!A (Ascending) or D (Descending),...},Start Row Index]\n" + + " Here are a few examples of the above format:\n" + + " Sort the first sheet by the first column in ascending order and the second column in descending order. Start the sort at row 1.\n" + + " [0:{0!A,1!D}:1]\n" + + " Sort the second sheet by the first column in ascending order. Start the sort at row 3.\n" + + " [1:{0!A}:2]\n" + + " Sort the fourth sheet by the first column in ascending order, the second column in descending order and the fifth column in ascending order. Start the sort at row 1.\n" + + " [3:{0!A,1!D,4!A}:0]"; + + public static void main(String[] args) throws Exception { + FileInputStream inputStream = null; + List sortSheetParamsList = new ArrayList<>(); + try { + //Parse the arguments + for(int i=0;i< args.length;i++){ + switch(i){ + case 0: + LOG_FILENAME = args[i]; + LogUtil.initLogging(LOG_FILENAME); + LogUtil.initCleanLogging(LOG_FILENAME); + break; + case 1: + WORKBOOK_FILENAME = args[i]; + inputStream = new FileInputStream(WORKBOOK_FILENAME); + break; + default: + sortSheetParamsList.add(parseArgument(args[i])); + } + } + XSSFWorkbook workbook = new XSSFWorkbook(inputStream); + //Sort the workbook sheets + sortSheetParamsList.forEach(sortSheetParams -> {sortSheet(workbook, sortSheetParams);}); + inputStream.close(); + // Write the output to a new file + FileOutputStream outputStream = new FileOutputStream(FilenameUtils.getFullPath(WORKBOOK_FILENAME) + "sorted_" + + FilenameUtils.getBaseName(WORKBOOK_FILENAME) + + "." + FilenameUtils.getExtension(WORKBOOK_FILENAME)); + workbook.write(outputStream); + outputStream.close(); + }catch(IllegalArgumentException iex){ + log.error("Unable to process the arguments.",iex); + System.out.println("Unable to process the arguments."); + System.out.println(instructions); + System.exit(100); + } + catch (RuntimeException rex){ + log.error("An unexpected runtime exception occurred.",rex); + System.out.println("An unexpected runtime exception occurred."); + rex.printStackTrace(); + System.exit(200); + } catch (Exception ex){ + log.error("An unexpected exception occurred.",ex); + System.exit(300); + } + System.exit(0); + } + + private static void sortSheet(XSSFWorkbook workbook,SortSheetParams params){ + XSSFSheet sheet = workbook.getSheetAt(params.sheetIndex); + XSSFSheet newSheet = workbook.createSheet("sorted" + '_' + sheet.getSheetName()); + //Collect the rows in an array list + ArrayList rows = new ArrayList<>(); + for(Row row:sheet){ + if(row.getRowNum() < params.rowStartIndex) + continue; + rows.add(row); + } + //Sort the array list by the criteria + Collections.sort(rows, + (row1, row2) -> compareRows(params.columnSortList,row2,row1) + ); + //Write the sorted rows to the new sheet + int i = 0; + for(Row row:rows){ + copyRow(workbook,sheet,newSheet,row.getRowNum(),i); + i++; + } + //Remove rows from the original sheet + for(Row row:rows){ + sheet.removeRow(row); + } + //Copy sorted rows back to the original sheet. + i=params.rowStartIndex; + for(Row row:newSheet){ + copyRow(workbook,newSheet,sheet,row.getRowNum(),i); + i++; + } + //Delete the new sheet + workbook.removeSheetAt(workbook.getNumberOfSheets()-1); + } + + private static SortSheetParams parseArgument(String sheetSortArg){ + SortSheetParams sortSheetParams = new SortSheetParams(); + // Regular expression to match the format + String pattern = "\\[(\\d+)\\:\\{(.*?)\\}\\:(\\d+)\\]"; + // Compile the regular expression + Pattern r = Pattern.compile(pattern); + // Match the input against the regular expression + Matcher m = r.matcher(sheetSortArg); + log.info("Parse Argument:"); + if (m.find()) { + sortSheetParams.sheetIndex = Integer.parseInt(m.group(1)); + sortSheetParams.columnSortList = parseColumnSortArg(m.group(2)); + sortSheetParams.rowStartIndex = Integer.parseInt(m.group(3)); + log.info("....sheetIndex:" + sortSheetParams.sheetIndex); + log.info("....columnSortList:" + m.group(2)); + log.info("....rowStartIndex:" + sortSheetParams.rowStartIndex); + } + return sortSheetParams; + } + + private static List parseColumnSortArg(String columnSortArg){ + List columnSortList = new ArrayList<>(); + // Split the second value string by "," + String[] secondValuePairs = columnSortArg.split(","); + ColumnSort colSort; + for (String pair : secondValuePairs) { + // Split the pair by "!" + String[] keyValue = pair.split("!"); + int key = Integer.parseInt(keyValue[0]); + String value = keyValue[1]; + colSort = new ColumnSort(key,value.compareToIgnoreCase("A") == 0 ? ColumnSort.SortDirection.Ascending: ColumnSort.SortDirection.Descending); + columnSortList.add(colSort); + } + return columnSortList; + } + + + private static void swapRows(XSSFWorkbook workbook, XSSFSheet sheet,Row row1,Row row2){ + int row2Index = row2.getRowNum(); + int lastRow = sheet.getLastRowNum(); + //Create a row after row2 as placeholder + sheet.shiftRows(row2.getRowNum() + 1, row2Index == lastRow ? lastRow+1:lastRow, + 1, true, true); + //Copy row 1 into the new row + copyRow(workbook,sheet,sheet,row1.getRowNum(), row2.getRowNum() + 1); + //Remove row 1 + removeRow(sheet, row1.getRowNum()); + } + + private static int compareRows(List columnSortList,Row row,Row row2) { + int ret = 0; + for (ColumnSort columnSort : columnSortList) { + Cell cell1 = row.getCell(columnSort.getColumnIndex()); + Cell cell2 = row2.getCell(columnSort.getColumnIndex()); + ret = compareCells(cell1,cell2,columnSort.getSortDirection()); + if(ret != 0) break; + } + return ret; + } + + private static int compareCells(Cell row1Cell, Cell row2Cell, ColumnSort.SortDirection sortDirection) { + if (row1Cell == null || row2Cell == null) return 0; + int ret; + switch(row1Cell.getCellType()){ + case Cell.CELL_TYPE_BOOLEAN: + boolean bvalue1 = row1Cell.getBooleanCellValue(); + boolean bvalue2 = row2Cell.getBooleanCellValue(); + ret = Boolean.compare(bvalue2,bvalue1); + break; + case Cell.CELL_TYPE_STRING: + String svalue1 = row1Cell.getStringCellValue(); + String svalue2 = row2Cell.getStringCellValue(); + ret = svalue2.compareToIgnoreCase(svalue1); + break; + case Cell.CELL_TYPE_NUMERIC: + Double dvalue1 = row1Cell.getNumericCellValue(); + Double dvalue2 = row2Cell.getNumericCellValue(); + ret = dvalue2.compareTo(dvalue1); + break; + default: + ret = 0; + } + return sortDirection == ColumnSort.SortDirection.Ascending ? ret : -ret; + } + + private static void copyRow(XSSFWorkbook workbook, XSSFSheet srcWorksheet, XSSFSheet destWorksheet, int sourceRowNum, int destinationRowNum) { + // Get the source / new row + XSSFRow newRow = destWorksheet.getRow(destinationRowNum); + XSSFRow sourceRow = srcWorksheet.getRow(sourceRowNum); + + // If the row exist in destination, push down all rows by 1 else create a new row + if (newRow != null) { + destWorksheet.shiftRows(destinationRowNum, destWorksheet.getLastRowNum(), 1); + } else { + newRow = destWorksheet.createRow(destinationRowNum); + } + + // Loop through source columns to add to new row + for (int i = 0; i < sourceRow.getLastCellNum(); i++) { + // Grab a copy of the old/new cell + XSSFCell oldCell = sourceRow.getCell(i); + XSSFCell newCell = newRow.createCell(i); + + // If the old cell is null jump to next cell + if (oldCell == null) { + newCell = null; + continue; + } + + // Copy style from old cell and apply to new cell + XSSFCellStyle newCellStyle = workbook.createCellStyle(); + newCellStyle.cloneStyleFrom(oldCell.getCellStyle()); + + newCell.setCellStyle(newCellStyle); + + // If there is a cell comment, copy + if (oldCell.getCellComment() != null) { + newCell.setCellComment(oldCell.getCellComment()); + } + + // If there is a cell hyperlink, copy + if (oldCell.getHyperlink() != null) { + newCell.setHyperlink(oldCell.getHyperlink()); + } + + // Set the cell data type + newCell.setCellType(oldCell.getCellType()); + + // Set the cell data value + switch (oldCell.getCellType()) { + case Cell.CELL_TYPE_BLANK: + newCell.setCellValue(oldCell.getStringCellValue()); + break; + case Cell.CELL_TYPE_BOOLEAN: + newCell.setCellValue(oldCell.getBooleanCellValue()); + break; + case Cell.CELL_TYPE_ERROR: + newCell.setCellErrorValue(oldCell.getErrorCellValue()); + break; + case Cell.CELL_TYPE_FORMULA: + newCell.setCellFormula(oldCell.getCellFormula()); + break; + case Cell.CELL_TYPE_NUMERIC: + newCell.setCellValue(oldCell.getNumericCellValue()); + break; + case Cell.CELL_TYPE_STRING: + newCell.setCellValue(oldCell.getRichStringCellValue()); + break; + } + } + + // If there are are any merged regions in the source row, copy to new row + for (int i = 0; i < srcWorksheet.getNumMergedRegions(); i++) { + CellRangeAddress cellRangeAddress = srcWorksheet.getMergedRegion(i); + if (cellRangeAddress.getFirstRow() == sourceRow.getRowNum()) { + CellRangeAddress newCellRangeAddress = new CellRangeAddress(newRow.getRowNum(), + (newRow.getRowNum() + + (cellRangeAddress.getLastRow() - cellRangeAddress.getFirstRow() + )), + cellRangeAddress.getFirstColumn(), + cellRangeAddress.getLastColumn()); + destWorksheet.addMergedRegion(newCellRangeAddress); + } + } + } + + /** + * Remove a row by its index + * @param sheet an Excel sheet + * @param rowIndex a 0 based index of the row being removed + */ + public static void removeRow(XSSFSheet sheet, int rowIndex) { + int lastRowNum=sheet.getLastRowNum(); + if(rowIndex>=0&&rowIndex columnSortList; + int rowStartIndex; + } +} diff --git a/src/main/java/com/ka/spreadsheet/util/LogUtil.java b/src/main/java/com/ka/spreadsheet/util/LogUtil.java new file mode 100644 index 0000000..a301725 --- /dev/null +++ b/src/main/java/com/ka/spreadsheet/util/LogUtil.java @@ -0,0 +1,40 @@ +package com.ka.spreadsheet.util; + +import org.apache.log4j.*; + +public class LogUtil { + public static final String ROOT_DIRECTORY = "root_directory"; + + public static void initLogging(String logFileName){ + FileAppender fa = new FileAppender(); + fa.setName("FileLogger"); + String rd = System.getProperty(ROOT_DIRECTORY); + rd = rd == null ? "." : rd; + fa.setFile(rd + "/logs/" +logFileName); + fa.setLayout(new PatternLayout("%d %-5p [%c{1}] %m%n")); + fa.setThreshold(Level.DEBUG); + fa.setAppend(false); + fa.activateOptions(); + Logger.getRootLogger().addAppender(fa); + } + + public static void initCleanLogging(String logFileName){ + FileAppender fa = new FileAppender(); + fa.setName("FileDisplayLogger"); + String rd = System.getProperty(ROOT_DIRECTORY); + rd = rd == null ? "." : rd; + fa.setFile(rd + "/logs/" +logFileName); + fa.setLayout(new PatternLayout("%m%n")); + fa.setThreshold(Level.DEBUG); + fa.setAppend(false); + fa.activateOptions(); + Logger.getLogger("DisplayLogger").addAppender(fa); + ConsoleAppender ca = new ConsoleAppender(); + ca.setLayout(new PatternLayout("%m%n")); + ca.setThreshold(Level.DEBUG); + ca.activateOptions(); + Logger.getLogger("DisplayLogger").addAppender(ca); + + + } +} \ No newline at end of file