タツノオトシゴのブログ

主にJavaに関するものです。

POIの小ネタ - セルの入力規則の修正

以前に、POIの小ネタ - セルの入力規則の取得 - タツノオトシゴの日記で記載した、セルの入力規則の続き。

ver3.11から、入力規則が取得できるようになりました。

ただし、削除者、修正のメソッドが準備されていませんでした。

今回は、リフレクションで無理矢理、範囲を変更する方法の紹介。

削除については、どうも上手くいきませんでした。

セルの入力規則の範囲を変更する処理

【XSSF(Excel2007、xlsx)形式の場合】

  • OpenXMLのデータ型「CTDataValidations」に入力規則のデータ型を持ちます。
    • CTDataValidationsは、XSSFSheetのプライベートなフィールド「CTWorksheet worksheet」で保持している、リフレクションで取得します。
  • 各入力規則は、「CTDataValidation」で取得でき、設定されている範囲を持ちます。
  • 入力規則の範囲は、「List CTDataValiadtion#getSqref()」から取得できます。
    • これらは、CellRangeAddressListの各要素を文字列(CellRaggeAddress#formatAsString())にした形式です。
  • 書き換える範囲が見つかったら、新しい範囲を追加します。

【HSSF(Excel2000/2003、xls)形式の場合】

  • 「DataValidityTable」に入力規則のデータ型を持ちます。
    • DataValidityTableは、HSSFSheetのプライベートなフィールド「InternalSheet _sheet」で保持しているため、リフレクションで取得します。
  • 各入力規則は、レコードの一種であるため、RecordVisitorを実装して、取得します。
    • 「DVRecord」が実際の入力規則を保持するレコードです。
  • 「DvRecord#getCellRangeAddress()」で範囲を取得できます。
  • 書き換える範囲が見つかったら、既存の範囲を削除し、新しい範囲を追加します。
/**
 * 入力規則の範囲を更新する。
 * @param sheet シート
 * @param oldRegion 更新対象の範囲。
 * @param newRegion 新しい範囲。
 * @return true:更新完了。false:指定した範囲を持つ入力規則が見つからなかった場合。
 */
public static boolean updateDataValidationRegion(final Sheet sheet,
        final CellRangeAddressList oldRegion, final CellRangeAddressList newRegion) {
    
    ArgUtils.notNull(sheet, "sheet");
    ArgUtils.notNull(oldRegion, "oldRegion");
    ArgUtils.notNull(newRegion, "newRegion");
    
    if(sheet instanceof XSSFSheet) {
        
        final List<String> oldSqref = convertSqref(oldRegion);
        
        try {
            final XSSFSheet xssfSheet = (XSSFSheet) sheet;
            Field fWorksheet = XSSFSheet.class.getDeclaredField("worksheet");
            fWorksheet.setAccessible(true);
            CTWorksheet worksheet = (CTWorksheet) fWorksheet.get(xssfSheet);
            
            CTDataValidations dataValidations = worksheet.getDataValidations();
            if(dataValidations == null) {
                return false;
            }
            
            for(int i=0; i < dataValidations.getCount(); i++) {
                CTDataValidation dv = dataValidations.getDataValidationArray(i);
                
                // 規則の範囲を比較し、同じならば範囲を書き換える。
                @SuppressWarnings("unchecked")
                List<String> sqref = new ArrayList<>(dv.getSqref());
                if(equalsSqref(sqref, oldSqref)) {
                    List<String> newSqref = convertSqref(newRegion);
                    dv.setSqref(newSqref);
                    
                    // 設定し直す
                    dataValidations.setDataValidationArray(i, dv);
                
                    return true;
                }
                
            }
            
            return false;
            
        } catch(Exception e) {
            throw new RuntimeException("fail update DataValidation's Regsion.", e);
        }
        
    } else if(sheet instanceof HSSFSheet) {
        
        final HSSFSheet hssfSheet = (HSSFSheet) sheet;
        try {
            Field fWorksheet = HSSFSheet.class.getDeclaredField("_sheet");
            fWorksheet.setAccessible(true);
            InternalSheet worksheet = (InternalSheet) fWorksheet.get(hssfSheet);
            
            DataValidityTable dvt = worksheet.getOrCreateDataValidityTable();
            
            // シート内の入力規則のデータを検索して、一致するものがあれば書き換える。
            final AtomicBoolean updated = new AtomicBoolean(false);
            RecordVisitor visitor = new RecordVisitor() {
                
                @Override
                public void visitRecord(final Record r) {
                    if (!(r instanceof DVRecord)) {
                        return;
                    }
                    
                    final DVRecord dvRecord = (DVRecord) r;
                    final CellRangeAddressList region = dvRecord.getCellRangeAddress();
                    if(equalsRegion(region, oldRegion)) {
                        
                        // 一旦既存の範囲を削除する。
                        while(region.countRanges() != 0) {
                            region.remove(0);
                        }
                        
                        // 新しい範囲を追加する。
                        for(CellRangeAddress newRange : newRegion.getCellRangeAddresses()) {
                            region.addCellRangeAddress(newRange);
                        }
                        
                        updated.set(true);
                        return;
                    }
                }
            };
            
            dvt.visitContainedRecords(visitor);
            
            return updated.get();
            
        } catch(Exception e) {
            throw new RuntimeException("fail update DataValidation's Regsion.", e);
        }
    } else {
        throw new UnsupportedOperationException("not supported update dava validation's region for type " + sheet.getClass().getName());
    }
    
}

/**
 * CellRangeAddressを文字列形式のリストに変換する。
 * @since 0.5
 * @param region
 * @return
 */
private static List<String> convertSqref(final CellRangeAddressList region) {
    
    List<String> sqref = new ArrayList<>();
    for(CellRangeAddress range : region.getCellRangeAddresses()) {
        sqref.add(range.formatAsString());
    }
    
    return sqref;
    
}

/**
 * 文字列形式のセルの範囲が同じかどうか比較する。
 * @since 0.5
 * @param sqref1
 * @param sqref2
 * @return
 */
public static boolean equalsSqref(final List<String> sqref1, final List<String> sqref2) {
    
    if(sqref1.size() != sqref2.size()) {
        return false;
    }
    
    Collections.sort(sqref1);
    Collections.sort(sqref2);
    
    final int size = sqref1.size();
    for(int i=0; i < size; i++) {
        if(!sqref1.get(i).equals(sqref2.get(i))) {
            return false;
        }
    }
    
    return true;
    
}

/**
 * 文字列形式のセルの範囲が同じかどうか比較する。
 * @since 0.5
 * @param region1
 * @param region2
 * @return
 */
public static boolean equalsRegion(final CellRangeAddressList region1, final CellRangeAddressList region2) {
    
    return equalsSqref(convertSqref(region1), convertSqref(region2));
    
}

セルの入力規則の範囲を変更する処理の呼び出し方

JUnitのテスタ形式ですが、下記のようになります。

実際には、既存の範囲であるCellRangeAddressListの値をそのまま書き換えるようになると思います。

その場合、既存の範囲を残しておく必要があるため、CellRangeAddressList#copy()でインスタンスをコピーしておくと便利だと思います。

/**
 * {@link POIUtils#updateDataValidationRegion(Sheet, CellRangeAddressList, CellRangeAddressList)}
 * ・XSSF形式
 * @since 0.5
 */
@Test
public void testUpdateDataValidationRegion_xssf() throws Exception {
    
    Workbook workbook = WorkbookFactory.create(new FileInputStream("src/test/data/utils.xlsx"));
    Sheet sheet = workbook.getSheet("入力規則");
    
    // 範囲の定義
    CellRangeAddressList oldRegion = new CellRangeAddressList();
    oldRegion.addCellRangeAddress(new CellRangeAddress(4, 5, 2, 2));
    
    CellRangeAddressList newRegion = new CellRangeAddressList();
    newRegion.addCellRangeAddress(new CellRangeAddress(4, 7, 2, 2));
    
    // 既存の入力規則を取得し、範囲が同じものを探索し、書き換えます。
    boolean updated = false;
    List<? extends DataValidation> validations = sheet.getDataValidations();
    for(DataValidation dv : validations) {
        
        // 既存のCellRangeAddressListを書き換えるときは、copy()メソッドでクローンしておく。
        CellRangeAddressList region = dv.getRegions().copy();
        if(POIUtils.equalsRegion(region, oldRegion)) {
            updated = POIUtils.updateDataValidationRegion(sheet, region, newRegion);
            break;
        }
    }
    
    assertThat(updated, is(true));
    
    // 書き換わったかどうか確認する
    boolean found = false;
    List<? extends DataValidation> updatedvalidations = sheet.getDataValidations();
    for(DataValidation dv : updatedvalidations) {
        CellRangeAddressList region = dv.getRegions();
        if(POIUtils.equalsRegion(region, newRegion)) {
            found = true;
            break;
        }
    }
    
    assertThat(found, is(true));
    
    workbook.write(new FileOutputStream("src/test/out/utils_out.xlsx"));
    
}