DefaultCellCommentHandler.java

package com.gh.mygreen.xlsmapper;

import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Optional;

import org.apache.poi.hssf.usermodel.HSSFRichTextString;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.ClientAnchor;
import org.apache.poi.ss.usermodel.Comment;
import org.apache.poi.ss.usermodel.CreationHelper;
import org.apache.poi.ss.usermodel.Drawing;
import org.apache.poi.ss.usermodel.RichTextString;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFRichTextString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.gh.mygreen.xlsmapper.annotation.XlsCommentOption;
import com.gh.mygreen.xlsmapper.util.ArgUtils;
import com.gh.mygreen.xlsmapper.util.CellPosition;

/**
 * {@link CellCommentHandler}の標準の実装。
 *
 * @since 2.1
 * @author T.TSUCHIE
 *
 */
public class DefaultCellCommentHandler implements CellCommentHandler {

    private static final Logger logger = LoggerFactory.getLogger(DefaultCellCommentHandler.class);
    
    /**
     * コメントの縦方向の開始位置。
     * 行数分で表現する。
     */
    private int vertialPrefix = 1;
    
    /**
     * コメントの横方向の開始位置。
     * 列数分で表現する。
     */
    private int horizontalPrefix = 1;
    
    /**
     * コメントの縦方向の最大サイズ。
     * 行数分で表現する。
     */
    private int maxVerticalSize = 4;
    
    /**
     * コメントの列方向の最大サイズ。
     * 列数分で表現する。
     */
    private int maxHorizontalSize = 3;
    
    @Override
    public Optional<String> handleLoad(final Cell cell, Optional<XlsCommentOption> commentOption) {
        
        Comment comment = getMergedCellComment(cell);
        if(comment == null) {
            return Optional.empty();
        }
        
        String commentText = comment.getString().getString();
        return Optional.of(commentText);
    }
    
    /**
     * 結合を考慮したセルのコメントを取得する。
     * @param cell 元となるセル。
     * @return コメント。コメントが設定されていなければ、nullを返す。
     */
    private Comment getMergedCellComment(final Cell cell) {
        Comment comment = cell.getCellComment();
        if(comment != null) {
            return comment;
        }
        
        final Sheet sheet = cell.getSheet();
        final int size = sheet.getNumMergedRegions();
        
        for(int i=0; i < size; i++) {
            final CellRangeAddress range = sheet.getMergedRegion(i);
            if(!range.isInRange(cell)) {
                continue;
            }
            
            // nullでないセルを取得する。
            for(int rowIdx=range.getFirstRow(); rowIdx <= range.getLastRow(); rowIdx++) {
                final Row row = sheet.getRow(rowIdx);
                if(row == null) {
                    continue;
                }

                for(int colIdx=range.getFirstColumn(); colIdx <= range.getLastColumn(); colIdx++) {
                    final Cell valueCell = row.getCell(colIdx);
                    if(valueCell == null) {
                        continue;
                    }

                    comment = valueCell.getCellComment();
                    if(comment != null) {
                        return comment;
                    }
                }
            }
        }
        
        return null;
        
    }

    @Override
    public void handleSave(final Cell cell, final Optional<String> text, final Optional<XlsCommentOption> commentOption) {
        
        if(!text.isPresent()) {
            // コメントが空のとき
            commentOption.ifPresent(option -> {
                if(option.removeIfEmpty()) {
                    // コメントが空のとき既存のコメントを削除する
                    cell.removeCellComment();
                }
            });
            return;
        }
        
        final Sheet sheet = cell.getSheet();
        final CreationHelper helper = sheet.getWorkbook().getCreationHelper();
        final Drawing<?> drawing = sheet.createDrawingPatriarch();
        
        final Comment comment;
        RichTextString richText = helper.createRichTextString(text.get());
        if(cell.getCellComment() == null) {
            ClientAnchor anchor = createAnchor(drawing, text.get(), cell, commentOption);
            comment = drawing.createCellComment(anchor);
            applyCommentFormat(richText, cell);
        } else {
            // 既存のコメントが存在する場合は、書式やサイズをコピーして使用する。
            comment = cell.getCellComment();
            RichTextString orgText = comment.getString();
            if(orgText.numFormattingRuns() > 0) {
                copyCommentFormat(richText, orgText);
            } else {
                applyCommentFormat(richText, cell);
            }
        }
        
        comment.setString(richText);
        
        // コメントの表示状態の更新
        commentOption.ifPresent(option -> comment.setVisible(option.visible()));
        
        cell.setCellComment(comment);
        
    }
    
    /**
     * コメントの位置、サイズを作成する。
     * @param drawing
     * @param text 書き込むコメント
     * @param cell 書込み対象のセル
     * @param commentOption コメントのオプション
     * @return コメントの表示位置
     */
    protected ClientAnchor createAnchor(final Drawing<?> drawing, final String text, final Cell cell,
            final Optional<XlsCommentOption> commentOption) {
        final CellPosition address = CellPosition.of(cell);
        
        // コメントを開業で分割し、最長の行を取得する。
        String[] split = text.split("\r\n|\r|\n");
        int maxLength = Arrays.stream(split)
                .mapToInt(str -> str.getBytes(Charset.forName("Windows-31j")).length)
                .max().orElse(0);
        
        /*
         * コメントの横サイズ。文字数(バイト数)をもとに決定。
         * ・1セルの文字数を元に出す。
         * ・columnWidthは、1文字の幅を1/256にしたものが単位となる。
         * ・最大3列分とする。
         */
        int charPerColumn = cell.getSheet().getColumnWidth(cell.getColumnIndex())/256;
        int commentColumnSize = (int)Math.ceil(maxLength*1.0 / charPerColumn);
        
        int columnSize = commentColumnSize;
        int lineWrappingCount = 0;
        if(commentColumnSize > maxHorizontalSize) {
            columnSize = maxHorizontalSize;
            // 行の折り返し回数を計算する
            lineWrappingCount = commentColumnSize / maxHorizontalSize;
        }
        
        if(commentOption.isPresent() && commentOption.get().horizontalSize() > 0) {
            // 直接指定されている場合
            columnSize = commentOption.get().horizontalSize();
            // 行の折り返し回数を計算する
            lineWrappingCount = columnSize / maxHorizontalSize;
        }
        
        // コメントの縦サイズ。行数をもとに決定。
        int rowSize = split.length + lineWrappingCount > maxVerticalSize ? maxVerticalSize : split.length + lineWrappingCount;
        if(commentOption.isPresent() && commentOption.get().verticalSize() > 0) {
            // 直接指定されている場合
            rowSize = commentOption.get().verticalSize();
        }
        
        return drawing.createAnchor(
                0, 0, 0, 0,
                address.getColumn() + horizontalPrefix, address.getRow() + vertialPrefix,
                address.getColumn() + horizontalPrefix + columnSize, address.getRow() + vertialPrefix + rowSize);
    }
    
    /**
     * 新規にコメントの装飾を設定する。
     * セルの装飾に合わせる。
     * 
     * @param toRichText 設定先のコメント
     * @param cell コメントを設定する先のセル
     */
    protected void applyCommentFormat(final RichTextString toRichText, final Cell cell) {
        
        toRichText.applyFont(cell.getSheet().getWorkbook().getFontAt(cell.getCellStyle().getFontIndexAsInt()));
        
    }
    
    /**
     * 既にコメントが設定されているときのコメントの装飾を設定する。
     * 既存のコメントの装飾をコピーするが、そのとき、1つ目のフォント設定のみとする。
     * 
     * @param toRichText コピー先
     * @param fromrichText コピー元
     */
    protected void copyCommentFormat(final RichTextString toRichText, final RichTextString fromrichText) {
        
        if(toRichText instanceof XSSFRichTextString) {
            toRichText.applyFont(((XSSFRichTextString)fromrichText).getFontOfFormattingRun(0));
            
        } else if(toRichText instanceof HSSFRichTextString) {
            toRichText.applyFont(((HSSFRichTextString)fromrichText).getFontOfFormattingRun(0));
            
        } else {
            logger.warn("not suuported exdcel format comment : {}", toRichText.getClass().getName());
        }
        
    }

    /**
     * コメントの縦方向の開始位置を取得する。
     * 行数分で表現する。
     * @return
     */
    public int getVertialPrefix() {
        return vertialPrefix;
    }

    /**
     * コメントの縦方向の開始位置を設定する。
     * 行数分で表現する。
     * @param vertialPrefix コメントの縦方向の開始位置。(0以上)
     * @throws IllegalArgumentException {@literal vertialPrefix < 0}
     */
    public void setVertialPrefix(int vertialPrefix) {
        ArgUtils.notMin(vertialPrefix, 0, "vertialPrefix");
        this.vertialPrefix = vertialPrefix;
    }

    /**
     * コメントの横方向の開始位置を取得する。
     * 列数分で表現する。
     * @return
     */
    public int getHorizontalPrefix() {
        return horizontalPrefix;
    }

    /**
     * コメントの横方向の開始位置を設定する。
     * 列数分で表現する。
     * @param horizontalPrefix コメントの横方向の開始位置。(0以上)
     * @throws IllegalArgumentException {@literal horizontalPrefix < 0}
     */
    public void setHorizontalPrefix(int horizontalPrefix) {
        ArgUtils.notMin(horizontalPrefix, 0, "horizontalPrefix");
        this.horizontalPrefix = horizontalPrefix;
    }
    
    /**
     * コメントの縦方向の最大サイズを取得する。
     * 行数分で表現する。
     * @return the maxVerticalSize
     */
    public int getMaxVerticalSize() {
        return maxVerticalSize;
    }

    
    /**
     * コメントの縦方向の最大サイズを設定する。
     * 行数分で表現する。
     * @param maxVerticalSize コメントの縦方向の最大サイズ。(1以上)
     * @throws IllegalArgumentException {@literal maxVerticalSize < 1}
     */
    public void setMaxVerticalSize(int maxVerticalSize) {
        ArgUtils.notMin(maxVerticalSize, 1, "maxVerticalSize");
        this.maxVerticalSize = maxVerticalSize;
    }

    
    /**
     * コメントの列方向の最大サイズ。
     * 列数分で表現する。
     * maxHorizontalSize を取得する
     * @return the maxHorizontalSize
     */
    public int getMaxHorizontalSize() {
        return maxHorizontalSize;
    }

    
    /**
     * コメントの列方向の最大サイズ。
     * 列数分で表現する。
     * @param maxHorizontalSize コメントの横方向の最大サイズ。(1以上)
     * @throws IllegalArgumentException {@literal maxHorizontalSize < 1}
     */
    public void setMaxHorizontalSize(int maxHorizontalSize) {
        ArgUtils.notMin(maxHorizontalSize, 1, "maxHorizontalSize");
        this.maxHorizontalSize = maxHorizontalSize;
    }

    
}