All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vaadin.flow.component.spreadsheet.GroupingUtil Maven / Gradle / Ivy

There is a newer version: 24.6.0
Show newest version
/**
 * Copyright 2000-2024 Vaadin Ltd.
 *
 * This program is available under Vaadin Commercial License and Service Terms.
 *
 * See {@literal } for the full
 * license.
 */
package com.vaadin.flow.component.spreadsheet;

import java.io.Serializable;
import java.lang.reflect.Method;

import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCol;
import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCols;
import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTRow;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Class that contains modified {@link XSSFSheet} methods regarding grouping.
 * The public methods here are entrypoints, other methods are copied only if
 * they needed changes.
 *
 * @author Thomas Mattsson / Vaadin Ltd.
 */
class GroupingUtil implements Serializable {

    private static final Logger LOGGER = LoggerFactory
            .getLogger(GroupingUtil.class);

    private GroupingUtil() {
    }

    public static void expandRow(XSSFSheet sheet, int rowNumber) {
        if (rowNumber == -1) {
            return;
        }
        XSSFRow row = sheet.getRow(rowNumber);
        // If it is already expanded do nothing.
        if (!row.getCTRow().isSetHidden()) {
            return;
        }

        // Find the start of the group.
        int startIdx = findStartOfRowOutlineGroup(sheet, rowNumber);

        // Find the end of the group.
        int endIdx = findEndOfRowOutlineGroup(sheet, rowNumber);

        // expand:
        // collapsed must be unset
        // hidden bit gets unset _if_ surrounding groups are expanded you can
        // determine
        // this by looking at the hidden bit of the enclosing group. You will
        // have
        // to look at the start and the end of the current group to determine
        // which
        // is the enclosing group
        // hidden bit only is altered for this outline level. ie. don't
        // un-collapse contained groups
        short level = row.getCTRow().getOutlineLevel();
        if (!isRowGroupHiddenByParent(sheet, rowNumber)) {

            /** change start */
            // start and end are off by one because POI did edge detection. Move
            // start back to correct pos:
            startIdx++;
            // end is already correct because of another bug (using '<' instead
            // of '<=' below)
            /** change end */

            for (int i = startIdx; i < endIdx; i++) {
                XSSFRow r = sheet.getRow(i);
                if (level == r.getCTRow().getOutlineLevel()) {
                    r.getCTRow().unsetHidden();
                } else if (!isRowGroupOrParentCollapsed(sheet, i, level)) {
                    r.getCTRow().unsetHidden();
                }
            }

        }

        // Write collapse field
        /** start */
        if (isRowsInverted(sheet)) {
            XSSFRow r = sheet.getRow(startIdx - 1);
            if (r != null && r.getCTRow().getCollapsed()) {
                r.getCTRow().unsetCollapsed();
            }
        } else {
            CTRow ctRow = sheet.getRow(endIdx).getCTRow();
            // This avoids an IndexOutOfBounds if multiple nested groups are
            // collapsed/expanded
            if (ctRow.getCollapsed()) {
                ctRow.unsetCollapsed();
            }
        }
        /** end */
    }

    private static int findEndOfRowOutlineGroup(XSSFSheet sheet, int row) {
        short level = sheet.getRow(row).getCTRow().getOutlineLevel();
        int currentRow;
        /** start */
        int lastRowNum = sheet.getLastRowNum() + 1;
        /** end */
        for (currentRow = row; currentRow < lastRowNum; currentRow++) {
            XSSFRow row2 = sheet.getRow(currentRow);
            if (row2 == null || row2.getCTRow().getOutlineLevel() < level) {
                break;
            }
        }
        return currentRow;
    }

    /**
     * Replaces {@link XSSFSheet#isRowGroupCollapsed(XSSFSheet, int)}, which
     * doesn't account for intermediary levels being collapsed or not.
     */
    private static boolean isRowGroupOrParentCollapsed(XSSFSheet sheet, int row,
            int originalLevel) {

        int level = sheet.getRow(row).getCTRow().getOutlineLevel();

        // start from row level and work upwards to original level
        while (level > originalLevel) {

            int collapseRow;
            if (isRowsInverted(sheet)) {
                collapseRow = findStartOfRowOutlineGroup(sheet, row);
                row--;
            } else {
                collapseRow = findEndOfRowOutlineGroup(sheet, row);
                row++;
            }

            if (sheet.getRow(collapseRow) != null) {

                CTRow ctRow = sheet.getRow(collapseRow).getCTRow();

                level = ctRow.getOutlineLevel();

                boolean collapsed = ctRow.getCollapsed();
                if (collapsed && ctRow.getOutlineLevel() >= originalLevel) {
                    // this parent is collapsed
                    return true;
                }
            }

        }
        return false;

    }

    private static boolean isRowGroupHiddenByParent(XSSFSheet sheet, int row) {
        // Look out outline details of end
        int endLevel;
        boolean endHidden;
        int endOfOutlineGroupIdx = findEndOfRowOutlineGroup(sheet, row);
        if (sheet.getRow(endOfOutlineGroupIdx) == null) {
            endLevel = 0;
            endHidden = false;
        } else {
            endLevel = sheet.getRow(endOfOutlineGroupIdx).getCTRow()
                    .getOutlineLevel();
            endHidden = sheet.getRow(endOfOutlineGroupIdx).getCTRow()
                    .getHidden();
        }

        // Look out outline details of start
        int startLevel;
        boolean startHidden;
        /** start */
        int startOfOutlineGroupIdx = findStartOfRowOutlineGroup(sheet, row);
        /** end */
        if (startOfOutlineGroupIdx < 0
                || sheet.getRow(startOfOutlineGroupIdx) == null) {
            startLevel = 0;
            startHidden = false;
        } else {
            startLevel = sheet.getRow(startOfOutlineGroupIdx).getCTRow()
                    .getOutlineLevel();
            startHidden = sheet.getRow(startOfOutlineGroupIdx).getCTRow()
                    .getHidden();
        }
        if (endLevel > startLevel) {
            return endHidden;
        }
        return startHidden;
    }

    @SuppressWarnings("deprecation")
    private static boolean isColumnGroupHiddenByParent(XSSFSheet sheet,
            int idx) {
        CTCols cols = sheet.getCTWorksheet().getColsArray(0);
        // Look out outline details of end
        int endLevel = 0;
        boolean endHidden = false;
        // int endOfOutlineGroupIdx = findEndOfColumnOutlineGroup(sheet,idx);
        int endOfOutlineGroupIdx = (Integer) callSheetMethod(
                "findEndOfColumnOutlineGroup", sheet, idx);
        CTCol[] colArray = cols.getColArray();
        /** start */
        if (endOfOutlineGroupIdx + 1 < colArray.length) {
            /** end */
            CTCol nextInfo = colArray[endOfOutlineGroupIdx + 1];
            if ((Boolean) callSheetMethod("isAdjacentBefore", sheet,
                    colArray[endOfOutlineGroupIdx], nextInfo)) {
                endLevel = nextInfo.getOutlineLevel();
                endHidden = nextInfo.getHidden();
            }
        }
        // Look out outline details of start
        int startLevel = 0;
        boolean startHidden = false;
        // int startOfOutlineGroupIdx = findStartOfColumnOutlineGroup(idx);
        int startOfOutlineGroupIdx = (Integer) callSheetMethod(
                "findStartOfColumnOutlineGroup", sheet, idx);
        if (startOfOutlineGroupIdx > 0) {
            CTCol prevInfo = colArray[startOfOutlineGroupIdx - 1];

            if ((Boolean) callSheetMethod("isAdjacentBefore", sheet, prevInfo,
                    colArray[startOfOutlineGroupIdx])) {
                startLevel = prevInfo.getOutlineLevel();
                startHidden = prevInfo.getHidden();
            }

        }
        if (endLevel > startLevel) {
            return endHidden;
        }
        return startHidden;
    }

    private static int findStartOfRowOutlineGroup(XSSFSheet sheet,
            int rowIndex) {
        // Find the start of the group.
        short level = sheet.getRow(rowIndex).getCTRow().getOutlineLevel();
        int currentRow = rowIndex;
        while (sheet.getRow(currentRow) != null) {
            if (sheet.getRow(currentRow).getCTRow().getOutlineLevel() < level) {
                /** start */
                return currentRow;
                /** end */
            }
            currentRow--;
        }
        return currentRow;
    }

    public static void collapseRow(XSSFSheet sheet, int rowIndex) {
        XSSFRow row = sheet.getRow(rowIndex);
        if (row != null) {
            int startRow = findStartOfRowOutlineGroup(sheet, rowIndex);

            // Hide all the columns until the end of the group
            int lastRow = writeHidden(sheet, row, startRow, true);

            /** start */

            if (isRowsInverted(sheet)) {
                if (sheet.getRow(startRow) != null) {
                    sheet.getRow(startRow).getCTRow().setCollapsed(true);
                } else if (startRow < 0) {
                    // happens when inverted group starts at 0; Excel does not
                    // write a collapsed prop for this case.
                } else {
                    XSSFRow newRow = sheet.createRow(startRow);
                    newRow.getCTRow().setCollapsed(true);
                }

            } else {
                if (sheet.getRow(lastRow) != null) {
                    sheet.getRow(lastRow).getCTRow().setCollapsed(true);
                } else {
                    XSSFRow newRow = sheet.createRow(lastRow);
                    newRow.getCTRow().setCollapsed(true);
                }
            }
            /** end */
        }
    }

    private static boolean isRowsInverted(XSSFSheet sheet) {
        boolean inverted = false;
        try {
            inverted = sheet.getCTWorksheet().getSheetPr().getOutlinePr()
                    .isSetSummaryBelow();
        } catch (NullPointerException IGNORE) {
            // fine
        }

        return inverted;
    }

    private static int writeHidden(XSSFSheet sheet, XSSFRow xRow, int rowIndex,
            boolean hidden) {
        short level = xRow.getCTRow().getOutlineLevel();

        /** completely rewritten after this line */
        // row index is the first row BEFORE group, not what we want
        rowIndex++;

        // row will be null at some point, this is safe
        while (true) {
            XSSFRow row = sheet.getRow(rowIndex);
            if (row == null) {
                break;
            }

            short outlineLevel = row.getCTRow().getOutlineLevel();
            if (outlineLevel < level) {
                break;
            }
            row.getCTRow().setHidden(hidden);
            rowIndex++;
        }

        /** old code, for reference */
        // for (Iterator it = sheet.rowIterator(); it.hasNext();) {
        // xRow = (XSSFRow) it.next();
        //
        // // skip rows before the start of this group
        // if (xRow.getRowNum() < rowIndex) {
        // continue;
        // }
        //
        // if (xRow.getCTRow().getOutlineLevel() >= level) {
        // xRow.getCTRow().setHidden(hidden);
        // rowIndex++;
        // }
        //
        // }
        return rowIndex;
    }

    public static void collapseColumn(XSSFSheet sheet, int columnNumber) {
        CTCols cols = sheet.getCTWorksheet().getColsArray(0);
        CTCol col = sheet.getColumnHelper().getColumn(columnNumber, false);
        int colInfoIx = sheet.getColumnHelper().getIndexOfColumn(cols, col);
        if (colInfoIx == -1) {
            return;
        }
        // Find the start of the group.
        int groupStartColInfoIx = (Integer) callSheetMethod(
                "findStartOfColumnOutlineGroup", sheet, colInfoIx);

        /** START */
        // Hide all the columns until the end of the group
        int lastColMax = (Integer) callSheetMethod("setGroupHidden", sheet,
                new Object[] { groupStartColInfoIx, col.getOutlineLevel(),
                        true });
        /** END */

        // write collapse field
        callSheetMethod("setColumn", sheet,
                new Object[] { lastColMax + 1, 0, null, null, Boolean.TRUE });
    }

    public static short expandColumn(XSSFSheet sheet, int columnIndex) {
        CTCols cols = sheet.getCTWorksheet().getColsArray(0);
        CTCol col = sheet.getColumnHelper().getColumn(columnIndex, false);
        int colInfoIx = sheet.getColumnHelper().getIndexOfColumn(cols, col);

        int idx = (Integer) callSheetMethod("findColInfoIdx", sheet,
                new Object[] { (int) col.getMax(), colInfoIx });
        if (idx == -1) {
            return -1;
        }

        // If it is already expanded do nothing.
        if (!isColumnGroupCollapsed(sheet, idx)) {
            return -1;
        }

        // Find the start/end of the group.
        int startIdx = (Integer) callSheetMethod(
                "findStartOfColumnOutlineGroup", sheet, idx);
        int endIdx = (Integer) callSheetMethod("findEndOfColumnOutlineGroup",
                sheet, idx);

        // expand:
        // colapsed bit must be unset
        // hidden bit gets unset _if_ surrounding groups are expanded you can
        // determine
        // this by looking at the hidden bit of the enclosing group. You will
        // have
        // to look at the start and the end of the current group to determine
        // which
        // is the enclosing group
        // hidden bit only is altered for this outline level. ie. don't
        // uncollapse contained groups
        CTCol[] colArray = cols.getColArray();
        @SuppressWarnings("unused")
        CTCol columnInfo = colArray[endIdx];
        short expandedLevel = -1;
        if (!isColumnGroupHiddenByParent(sheet, idx)) {
            /** Start */
            short outlineLevel = col.getOutlineLevel();
            /** end */
            boolean nestedGroup = false;
            for (int i = startIdx; i <= endIdx; i++) {
                CTCol ci = colArray[i];
                if (outlineLevel == ci.getOutlineLevel()) {
                    ci.unsetHidden();
                    if (nestedGroup) {
                        nestedGroup = false;
                        ci.setCollapsed(true);
                    }
                    expandedLevel = outlineLevel;
                } else {
                    nestedGroup = true;
                }
            }
        }

        /** start */
        // // Write collapse flag (stored in a single col info record after this
        // // outline group)
        // callSheetMethod("setColumn", sheet,
        // new Object[] { (int) columnInfo.getMax() + 1, null, null,
        // Boolean.FALSE, Boolean.FALSE });
        /** end */
        return expandedLevel;
    }

    private static boolean isColumnGroupCollapsed(XSSFSheet sheet, int idx) {

        /**
         * The APIDoc for this method says that cols work as rows, with the
         * 'collapsed' attribute being after the col group. It isn't, the
         * 'hidden' attribute is used instead. Hence, rewrite:
         */

        CTCols cols = sheet.getCTWorksheet().getColsArray(0);

        CTCol col = cols.getColArray(idx);
        return col.isSetHidden();

        /**
         * original code for reference
         */

        // CTCols cols = sheet.getCTWorksheet().getColsArray(0);
        // CTCol[] colArray = cols.getColArray();
        // int endOfOutlineGroupIdx = findEndOfColumnOutlineGroup(sheet, idx);
        // int nextColInfoIx = endOfOutlineGroupIdx + 1;
        // if (nextColInfoIx >= colArray.length) {
        // return false;
        // }
        // CTCol nextColInfo = colArray[nextColInfoIx];
        //
        // CTCol col = colArray[endOfOutlineGroupIdx];
        // if (!isAdjacentBefore(col, nextColInfo)) {
        // return false;
        // }
        //
        // return nextColInfo.getCollapsed();
    }

    /**
     * Util method so that we don't need to copy all private methods from
     * XSSFSheet.
     */
    private static Object callSheetMethod(String methodname, XSSFSheet sheet,
            Object... params) {

        Class[] paramtypes = new Class[params.length];
        for (int i = 0; i < params.length; i++) {
            paramtypes[i] = params[i] == null ? Object.class
                    : params[i].getClass();
        }

        Method method = null;
        try {

            for (Method m : XSSFSheet.class.getDeclaredMethods()) {
                if (m.getName().equals(methodname)) {
                    method = m;
                }
            }

            // method = XSSFSheet.class.getDeclaredMethod(methodname,
            // paramtypes);
            if (method != null) {
                method.setAccessible(true);
                return method.invoke(sheet, params);
            }

        } catch (Exception e) {
            LOGGER.info("Error accessing method: " + method, e);
        } finally {
            if (method != null) {
                method.setAccessible(false);
            }
        }

        return null;
    }

    /**
     * @return A column index, which can uniquely identify the group that exists
     *         at the given col, and has the given level. (col
     *         might have a level that is higher than we want). 1-based.
     */
    public static long findUniqueColIndex(CTCols colsArray, CTCol col,
            short lastlevel) {
        int index = colsArray.getColList().indexOf(col);
        for (; index < colsArray.sizeOfColArray(); index++) {

            CTCol current = colsArray.getColArray(index);
            if (current.getOutlineLevel() == lastlevel) {
                return current.getMin();
            }
        }
        return -1;
    }

    /**
     * @return A row index, which can uniquely identify the group that exists
     *         between the given indexes, and has the given level. (the row at
     *         start might have a level that is higher than we
     *         want). 0-based.
     */
    public static long findUniqueRowIndex(Spreadsheet sheet, int start, int end,
            int lastlevel) {
        for (int i = start; i <= end; i++) {

            XSSFRow current = (XSSFRow) sheet.getActiveSheet().getRow(i);
            if (current.getCTRow().getOutlineLevel() == lastlevel) {
                return i;
            }
        }
        return -1;
    }

    /**
     * @return If the group that spans the given col and has the given level is
     *         hidden or not. (col might have a higher level than the one we
     *         want to check).
     */
    public static boolean checkHidden(CTCols colsArray, CTCol col,
            short lastlevel) {
        int index = colsArray.getColList().indexOf(col);
        for (; index < colsArray.sizeOfColArray(); index++) {
            CTCol current = colsArray.getColArray(index);
            if (current.getOutlineLevel() == lastlevel) {
                return current.isSetHidden();
            }
        }
        return false;
    }

    /**
     * @return The end index of the row group that spans the given row, with the
     *         given level. 0-based.
     */
    public static long findEndOfRowGroup(Spreadsheet sheet, int rowindex,
            XSSFRow row, short level) {

        while (rowindex < sheet.getRows()) {
            XSSFRow r = (XSSFRow) sheet.getActiveSheet().getRow(rowindex);
            if (r == null || r.getCTRow().getOutlineLevel() < level) {
                // end
                return rowindex - 1l;
            }

            rowindex++;
        }
        return -1l;
    }

    /**
     * @return The end index of the col group that spans the given col, with the
     *         given level. 1-based.
     */
    public static long findEndOfColGroup(CTCols colsArray, CTCol col,
            short level) {

        CTCol previous = null;

        int index = colsArray.getColList().indexOf(col);
        for (; index < colsArray.sizeOfColArray(); index++) {

            CTCol c = colsArray.getColArray(index);

            // break in cols or smaller outline
            boolean hasBreak = previous != null
                    && c.getMin() - previous.getMax() > 1;
            if (hasBreak || c.getOutlineLevel() < level) {
                break;
            }
            previous = c;
        }

        // group ends on last
        return previous == null ? 0 : previous.getMax();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy