タツノオトシゴのブログ

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

Javadocのコードにシンタックスハイライトを適用する

Javadocで、<pre>〜</pre>タグを使って、サンプルなどのコードを記述する場合、デフォルトのままだと、通常の文章と区別が付きづらくわかりづらい。

そのため、シンタックスハイライトを適用したかったので、試したことをメモしておく。
今回は、mavenhighlight.jsを使う。

実際のサンプルは下記を参考。

Javadocの記述

不要なpreタグにシンタクスハイライトが当たらないように、<pre class="highlight"><code class="java">〜</code></pre>のように、preタグにもclass属性を適用する。

/*
 * クラスの説明
 *
 * <pre class="highlight"><code class="java">
 * // コードの記述
 * ・・・
 * </code></pre>
 */

stylesheet.cssの記述

javadocの標準のCSSの場合、preタグには既にCSSが適用されているため、文字の大きさなど少しカスタマイズしたものを利用する。

通常に生成したJavadoc中の「stylesheet.css」の最後に下記の内容を追加して、「src/javadoc」以下に格納しておく。

〜省略
/**
 * 追加したCSS
 */
pre.highlight {
    margin-top: 10px;
    margin-bottom: 10px;
    font-size: 80%;
    border: solid 1px #9eadc0;
    background-color: #f9f9f9;
}

pom.xmlの記述

Javadocのレポートの記述の

タグ内に、シンタックスハイライトを適用するためのJavaScriptを記述する。

タグは、Javadocの見出しを設定するものなので、JavaScriptだけ記述すると、HTMLを生成した場合余分なスペースが表示されるため、ライブラリ名などを出力するように定義しておくようにする。

<reporting>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-javadoc-plugin</artifactId>
            <version>2.10.3</version>
            <configuration>
                <source>${java.version}</source>
                <encoding>UTF-8</encoding>
                <charset>UTF-8</charset>
                <docencoding>UTF-8</docencoding>
                <locale>ja_JP</locale>
                <links>
                    <link>http://docs.oracle.com/javase/jp/7/api/</link>
                </links>
                <!-- 少しカスタマイズしたCSSを適用する。 -->
                <stylesheetfile>${basedir}/src/javadoc/stylesheet.css</stylesheetfile>

                <!-- Javadocの見出し出力定義にJavaScriptを記述する。 -->
                <header>${project.name} - ${project.version}
<![CDATA[
<link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.1.0/styles/default.min.css">
<script src="http://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.1.0/highlight.min.js"></script>
<script src="https://code.jquery.com/jquery-1.12.0.min.js"></script>
<script>
$(document).ready(function() {
$('pre.highlight code').each(function(i, block) {
hljs.highlightBlock(block);
});
});
</script>
]]>
                </header>

            </configuration>
        </plugin>
    </plugins>
<reporting>

XlsMapper v1.0のリリース

ExcelJavaマッピングするライブラリXlsBeansのクローンであるXlsMapper v1.0をようやくリリースしました。

XlsMapperは、主に次の機能を強化しています。詳細は、こちら

  1. 書き込み機能がある。
  2. 入力値検証機能がある。BeanValidationも利用できます。


4月から品質を高めるために、ずっとテスタを作ってました。
普通に使う分は問題ありませんが、かなりの量のバグがありました。


仕事で行うユニットテストを行っている感覚に陥って、途中何度も投げ出しそうになりました。


リリースノートを見ていただくとわかりますが、1.0の正式リリースにしたことは品質を高めることの他、ソースを整理しました。

あと、ちょっとした機能追加です。


開発中にあれもこれもほしい機能があって、中途半端になっていた部分を削除したり、他のライブラリに置き換えるなどしました。


次のリリースは、Java8対応をする予定です。

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"));
    
}

Eclipse4.4で古い形式のプラグインを動作させる

2014年にリリースされたElcipse4.4(Luna)は、古い形式のプラグインが標準ではサポートされなくなりました。

そのため、Velcoityエディタなど開発が止まっているようなプラグインは動作しなくなってしまいます。

対策としては、古い形式のプラグインを動作させる「Eclipse 2.0 Style Plugin Support」をインストールします。

では、よいEclipseライフを。

インストール手順

  1. Eclipseメニュー「Help」-「Install New Software」を選択する。
  2. プルダウンから「Eclipse Project Update」を選択する。
  3. カテゴリ「Eclipse Tests, Examples and Extras」の中の「Eclipse 2.0 Style Plugin Support」にチェックを入れ、インストールする。

excel-cellformatterのリリース

JExcelAPIやApache POIでExcelの書式に沿ってフォーマットするJavaのライブラリをリリースしました。

セルの書式が日付、会計の場合、JavaのSimpleDateFormatやDecimalFomratではサポートしていない書式記号があり、それらに対応したライブラリになります。

和暦、分数以外にも、漢数字、大字表示など日本語環境で使用可能な書式にも対応しています。

Excel2000〜2010で利用可能な全ての書式に対応しています。
さらに、LibreOfficeなので利用可能な一部の記号にも対応しています。

ただし、Cellの値を文字列としてフォーマットする機能のみで、文字列からオブジェクトに変換するパース機能はありません。

簡単な使い方

Apacehe POIの場合

Cellオブジェクトを「POICellFormatter#fomrat(...)」に渡すだけです。

  • OSの言語環境によって変わる書式があるため、Linux環境の場合は注意が必要です。
    • 引数にLocaleを直接指定することもできます。
  • POICellFormatterは書式オブジェクトをキャッシュするため、インスタンスを使い回しで利用します。
Cell cell = /* セルの値の取得 */;

final POICellFormatter cellFormatter = new POICellFormatter();

String contents = cellForrmatter.format(cell);

// ロケールを指定してフォーマットする。
contents = cellForrmatter.format(cell, Locale.JAPANESE);
Jexcel API

Cellオブジェクトを「JXLCellFormatter#format(...)」に渡します。

  • 日本語を含むExcelファイルの場合、文字コードを ISO8859_1 を指定します。
    • 指定しない場合は、会計の書式中の円記号 ¥ が文字化けします。
    • Windows-31j と指定しても文字化けするため、注意してください。
  • 1904年始まり設定がされているExelファイルの設定かどうか、メソッド

「JXLUtils.isDateStart1904(...)」 で調べた値を渡します。

    • 通常は1899年12月31日(Excel表記上は 1900年1月0日)が基準です。
    • JXLUtils.isDateStart1904(...)メソッドには、Sheetオブジェクトを引数にとるメソッドも用意されています。
// シートの読み込み
final WorkbookSettings settings = new WorkbookSettings();
settings.setSuppressWarnings(true);
settings.setGCDisabled(true);

// 文字コードを「ISO8859_1」にしないと、一部の文字が文字化けします。
settings.setEncoding("ISO8859_1");

final Workbook workbook = Workbook.getWorkbook(in, settings);

Cell cell = /* セルの値の取得 */;

final JXLCellFormatter cellFormatter = new JXLCellFormatter();

// JXLUtils.isDateStart1904(...)を利用して、1904年始まりのシートか調べる。
String contents = cellForrmatter.format(cell, JXLUtils.isDateStart1904(workbook));

// ロケールを指定してフォーマットする。
contents = cellForrmatter.format(cell, Locale.JAPANESE, JXLUtils.isDateStart1904(workbook));

Redmine2.x用のEclipse用のプラグイン「redmine-mylyn-plugin」

Redmine2.xの場合、公式サイトなどで紹介されているEclipse用のプラグインだと、「400 クライアントエラー」が発生してしまいます。


これは、Redmineの前提フレームワークであるRuby on RailsCSRF脆弱性対策が、Redmine2.xの前提となるバージョンで追加されたたため、認証エラーが発生しているようです。


対策としては、対策済みのプラグインに入れ替えれば動作します。
下記に手順を示します。


自分が試した環境は以下の通りです。

項目
Redmine Redmine 2.6.1(CentOS上で構築)
Eclipse Eclipse4.3.2(64bit)、Eclipse4.4.1(64bit)


【参考にしたサイト】

既存のプラグインのアンインストール

メニュー「Help」−「About Eclipse」を選択し、「Installation Details」ボタンを押下。

  • タブ「Installed Software」から、下記のものを選択し、「Uninstall」ボタンを押下する。
    • 削除する項目が2つあるので、2回繰り返す必要がある。
Mylyn Connector: Redmine
Mylyn Connector: Redmine -redmine-Plugin-Support
※ プラグインを選択した際に、下方のペインに「Depends on Redmine 1.0.0」と記載されているのを確認。
  • アンインストール後、Eclipseを再起動する。
  • 起動後、View「Task Repositories」の右クリックメニュー「Add Repository」から、「Redmine〜」が消えていればOK。

EclpseのRedmine2.x用のプラグインのインストール

  1. 下記のサイトのリンク「Zip with build plugin as P2 Repo」をクリックし、媒体をダウンロードする。
  2. メニュー「Help」−「Install New Software」から、ZIPファイルをインストールする。
    • ボタン「Add」を選択し、ボタン「Archive」を選択し、ダウンロードしたzipファイルを選択し、「OK」ボタンを押下する。
    • メニューに沿って、インストールする。バージョンが「0.4.0.XXX」になっていることを確認。
  3. 再起動後、View「Task Repositories」の右クリックメニュー「Add Repository」から、「Redmine〜」が表示されていればOK。

XlsMapper 0.3のリリース

XlsBeansのクローンののXlsMapper v0.3をリリースしました。

今回もマイナーな機能を追加です。

POI-3.11が先日に正式リリースされたので、そこで追加さた機能であるセルの入力規則を書き込み時に自動的に修正するようにしました。

@XlsHorizontalRecordsを使ってExcelファイルを出力する際に、レコードを追加、削除した際に入力規則を追従するようにしました。
入力規則は、データ構造上はセルにではなくシートに対して保持しているため、単純にセルをコピーするだけでは対応できません。

また、名前の定義の範囲も修正するようにしました。名前の定義の範囲は、何故かレコードの追加、削除した際にPOI側で自動的に範囲を修正してくれているようです。


これらの機能を有効にするには、XlsMapperConfigで設定を変更する必要があります。

XlsMapper xlsMapper = new XlsMapper();
// セルの入力規則の自動修正機能の有効化。
// この機能を使うにはPOI-3.11以上が必要。
// POIの機能が利用できなければ、この処理はスキップされます。
xlsMapper.getConig().setCorrectCellDataValidationOnSave(true);

// 名前の定義範囲の自動修正機能の有効化。
xlsMapper.getConig().setCorrectNameRangeOnSave(true);

xlsMapper.save(...);