POIの小ネタ - セルの「縮小して全体を表示する」の設定
POI ver3.10から、セルの「縮小して全体を表示」の設定が正式サポートされました。
今まで、「折り返して全体を表示する」は、CellStyle.setWrapText(boolean)でサポートされていたのに不思議です。
メソッドとして、CellStyle.setShrinkToFit(boolean)を使用します。
POI ver.3.9以前にも、実装されていましたが、そのメソッドは公開されていませんでした。
そのため、リフレクションでフィールドの値を取得し、無理矢理変更する方法があります。
【注意事項】
セルの設定の「縮小して全体を表示する」と「折り返して全体を表示する」はどちらか一方のみ有効にできます。
どちらも設定値をtrueにした場合、「折り返して全体を表示する」の設定が優先されるようです。
import org.apache.poi.hssf.record.ExtendedFormatRecord; import org.apache.poi.hssf.usermodel.HSSFCellStyle; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.usermodel.WorkbookFactory; import org.apache.poi.xssf.usermodel.XSSFCellStyle; import org.apache.poi.xssf.usermodel.extensions.XSSFCellAlignment; import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCellAlignment; // リフレクションを使い、「縮小して全体を表示する」を設定する。 public void setShrinkToFit(final Cell cell, boolean shrinkToFit) { final CellStyle style = cell.getCellStyle(); // POI-3.10以上の場合(正式対応) // try { // style.setShrinkToFit(shrinkToFit); // return; // } catch(Exception e) { // // POI-3.9以前の場合は、メソッドがないのでエラーが発生 // } try { //POI-3.10以降(リフレクションで呼ぶ) final Method method = style.getClass().getMethod("setShrinkToFit", boolean.class); method.setAccessible(true); method.invoke(style, shrinkToFit); cell.setCellStyle(style); return; } catch (Exception e) { } // POI-3.9以前の場合(リフレクションして呼ぶ) if(style instanceof HSSFCellStyle) { // POI-3.9以前のExcel2003形式 try { final Field field = style.getClass().getDeclaredField("_format"); field.setAccessible(true); ExtendedFormatRecord record = (ExtendedFormatRecord) field.get(style); record.setShrinkToFit(shrinkToFit); cell.setCellStyle(style); return; } catch (Exception e ) { } } else if(style instanceof XSSFCellStyle) { // POI-3.9以前のExcel2007形式 try { final Method aligngmentMethod = style.getClass().getDeclaredMethod("getCellAlignment"); aligngmentMethod.setAccessible(true); final XSSFCellAlignment alignment = (XSSFCellAlignment) aligngmentMethod.invoke(style); final Field alignmentField = alignment.getClass().getDeclaredField("cellAlignement"); alignmentField.setAccessible(true); CTCellAlignment alignment2 = (CTCellAlignment) alignmentField.get(alignment); alignment2.setShrinkToFit(shrinkToFit); cell.setCellStyle(style); return; } catch (Exception e ) { } } }
POIの小ネタ - セルの入力規則の取得
現在、開発中のPOI ver3.11から、セルの入力規則の取得ができるようになりました。
今まで、入力規則の設定はできていましたが、取得はできませんでした。
取得するには、メソッドとして、Sheet.getDataValidations()を使用します。
取得できることにより、行を追加したときなど、既存の行の入力規則を変更することができるようになります。
【注意事項】
POI-3.11beta3で試しましたが、既存の入力規則の範囲を変更するには、新たに入力規則用のインスタンスであるDataValitaionを再作成し、再追加する必要があるようです。
入力範囲を書き換えるには、現状メソッドがないため、リフレクションなどを利用して無理矢理変更します。
詳細は、POIの小ネタ - セルの入力規則の修正 - タツノオトシゴの日記を参照。
import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.DataValidation; import org.apache.poi.ss.usermodel.DataValidationConstraint; import org.apache.poi.ss.usermodel.DataValidationHelper; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.usermodel.WorkbookFactory; import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.ss.util.CellRangeAddressList; public void fixValidationRule(final Cell cell) { // 行を1つ挿入する Sheet sheet = cell.getSheet(); int lastRow = sheet.getLastRowNum(); if(lastRow == cell.getRowIndex()) { lastRow++; } sheet.shiftRows(cell.getRowIndex()+1, lastRow, 1); DataValidationHelper helper = sheet.getDataValidationHelper(); // 入力規則の範囲を修正する List<? extends DataValidation> list = sheet.getDataValidations(); for(DataValidation validation : list) { // 自身のセルの入力規則を探し出す CellRangeAddressList region = validation.getRegions(); for(CellRangeAddress range : region.getCellRangeAddresses()) { if(range.isInRange(lastRow-1, cell.getColumnIndex())) { // 追加した行に対して入力規則の設定範囲を広げる(新たに領域を追加する) range.setLastRow(lastRow); break; } } // 規則を作り直して、再度シートに追加する // ※再追加すると既存の範囲が残ったままになるため、実際は、下記URLを参照。 // http://d.hatena.ne.jp/tatsu-no-toshigo/20150612/1434131054 sheet.addValidationData(helper.createValidation(validation.getValidationConstraint(), region)); } }
続・JavaFX8の印刷機能によるHTMLのPDF変換
以前の記事では失敗した、Java8の印刷機能を使ったHTMLのPDF変換を再度試してみた。
結果、上手くいきました。
【環境】
- OS:CentOS6.6 64bit
- kernel:2.6.32-504.el6.x86_64
- Java:build 1.8.0_25-b17
- 仮想プリンタ:cups-pdf(cups-pdf-2.6.1-4.el6.x86_64)
【はまったこと】
WebViewノードを印刷するときに、一般的な方法であるノードの印刷方法(PrinterJob#printPage)を使用すると、複数ページにまたがる場合や、縮小して印刷する際には自前で設定する必要がある。
専用のメソッドWebEngine#print()を使うと、自動的に縮小したりしてくれる。
さらに、CSSの印刷用メディアにも対応してくれる。
JavaFXの公式ページにも、このことは載っていました。
専用の「WebEngine#print」を使用した印刷
- cups-pdfを使用した場合は、ダイアログなしても印刷できる。
- 出力ファイル名は、プリンタのジョブ名になるため、ジョブ名を指定する。
- 今回は、デフォルトのプリンタをcups-pdfにしているが、通常は「Printer#getAllPrinters()」で全てのプリンタを取得して、目的のプリンタを取得する。
import java.io.Serializable; import javafx.application.Application; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.concurrent.Worker; import javafx.print.PageLayout; import javafx.print.PageOrientation; import javafx.print.Paper; import javafx.print.Printer; import javafx.print.PrinterJob; import javafx.scene.Scene; import javafx.scene.web.WebEngine; import javafx.scene.web.WebView; import javafx.stage.Stage; import javafx.stage.Window; public class Html2PdfApplication0 extends Application { public static void main(String[] args) { String url = "http://www.yahoo.co.jp/"; // url = "http://ja.wikipedia.org/wiki/JavaFX"; try { launch(new String[] {"--url=" + url}); } catch (Exception e) { e.printStackTrace(); } } @Override public void start(final Stage primaryStage) throws Exception { final Application.Parameters params = getParameters(); final String url = params.getNamed().get("url"); final WebView webView = new WebView(); final WebEngine engine = webView.getEngine(); webView.autosize(); final Scene scene = new Scene(webView); primaryStage.setScene(scene); engine.getLoadWorker().stateProperty().addListener(new ChangeListener<Worker.State>() { @Override public void changed(ObservableValue ov, Worker.State oldState, Worker.State newState) { if(newState == Worker.State.SUCCEEDED) { System.out.println("complete load."); try { saveNodeAsPdf(webView); // saveNodeAsPdfWithMultiple(webView, scene); } finally { // 印刷したらアプリケーションの終了 Platform.exit(); } } } }); engine.load(url); primaryStage.show(); } /** * 一般的な印刷用メディアで印刷する。 * @param webView */ private void saveNodeAsPdf(final WebView webView) { System.out.println("==== start print pdf"); // プリンタの取得 final Printer printer = Printer.getDefaultPrinter(); if(printer == null) { System.err.println("printe is null"); return; } else { System.out.println("print name:" + printer.getName()); } // レイアウトの設定 PageLayout pageLayout = printer.createPageLayout(Paper.A4, PageOrientation.PORTRAIT, Printer.MarginType.DEFAULT); // Printerジョブの取得。 // ジョブ名が出力するファイル名となるため変更する。 PrinterJob job = PrinterJob.createPrinterJob(printer); job.getJobSettings().setJobName("javafx2pdf"); System.out.printf("jobName [%s]\n", job.getJobSettings().getJobName()); if(job != null) { // CSSの印刷用メディア印刷するには、WebEngin#printを使用する。 webView.getEngine().print(job); job.endJob(); } System.out.println("==== end print pdf"); } }
一般的な「PrinterJob#printPage」を使用した印刷
非常に面倒です。
- PrinterJob#printPageメソッドは、表示されているそのままを印刷するメソッドのため、印刷使用とするページサイズに収まるように縮小表示する必要がある。
- さらにStage(Window)のウィンドウサイズを調整する必要がある。
- 現在のWebページのサイズやスクロール表示を考慮する必要がある。
- Webページのサイズは、WebEngine#executeScript()を使って、JavaScriptで取得する必要がある。
- 複数ページ印刷する場合、WebViewをスクロールする必要がある。
- WebViewは自動的にスクロールバーが表示されるが、これはJavaFXのScrollPaneではなく、WebKitの部品のものなので、JavaScriptでスクロールする必要がある。
非常に面倒ですが、WebView#snapshot()でWebページを画像として保存するも同様に、このような方法で色々と考慮する必要があります。
public class Html2PdfApplication0 extends Application { // 呼び出す部分は省略 /** * WebViewのノードに対して印刷する。 * 自分で縮小、スクロールする必要がある。 * @param webView * @param scene */ private void saveNodeAsPdfWithMultiple(final WebView webView, final Scene scene) { System.out.println("==== start print pdf"); /* * スクロールバーの非表示 * CSSは下記の内容 * body {overflow-x: hidden; overflow-y: hidden;} */ webView.getEngine().setUserStyleSheetLocation(getClass().getResource("no-scroll.css").toExternalForm()); // プリンタの取得 final Printer printer = Printer.getDefaultPrinter(); if(printer == null) { System.err.println("printe is null"); return; } else { System.out.println("print name:" + printer.getName()); } // 現在のウィンドウ情報を取得 final WebEngine engine = webView.getEngine(); WebViewSize viewSize = printWindowInfo(engine); // レイアウトの設定 PageLayout pageLayout = printer.createPageLayout( Paper.A4, PageOrientation.PORTRAIT, Printer.MarginType.DEFAULT ); // 用紙に合わせてWebViewを縮小表示する double scaleX = (pageLayout.getPrintableWidth() - pageLayout.getLeftMargin() - pageLayout.getRightMargin() ) / webView.getBoundsInParent().getWidth(); webView.setZoom(scaleX); System.out.printf("zoom size=%f\n", scaleX); // ウィンドウの横サイズを調整し、全て表示できるようにする Window stage = scene.getWindow(); int scrollBarWidth = 20; double windowWidth = viewSize.documentWidth * scaleX + scrollBarWidth; stage.setWidth(windowWidth); // Printerジョブの取得。 // ジョブ名が出力するファイル名となるため変更する。 final PrinterJob job = PrinterJob.createPrinterJob(printer); job.getJobSettings().setJobName("javafx2pdf"); System.out.printf("jobName [%s]\n", job.getJobSettings().getJobName()); if(job != null) { // 印刷するページ数のトータルページの計算 viewSize = printWindowInfo(engine); // 縮小表示した場合、現在のサイズを再取得する int pages = viewSize.documentHeight / viewSize.windowHeight; if(viewSize.documentHeight % viewSize.windowHeight != 0) { pages++; } // 複数ページの印刷 boolean success = false; for(int i=0; i < pages; i++) { if(i > 0) { // JavaScriptを使ってスクロール int scolled = i*viewSize.windowHeight; engine.executeScript(String.format("window.scrollTo(%d, %d);", 0, scolled)); } success = job.printPage(pageLayout, webView); System.out.printf("... page[%d/%d] - print result=%b\n", i+1, pages, success); if(!success) { job.endJob(); break; } } if(success) { job.endJob(); } } System.out.println("==== end print pdf"); } /** * JavaScriptで画面サイズ情報などを取得する * http://archiva.jp/web/javascript/get_page-size.html * */ WebViewSize printWindowInfo(WebEngine engine) { System.out.println("-------------------------"); // ウィンドウサイズの取得 int windowWidth = (Integer) engine.executeScript("document.documentElement.clientWidth || document.body.clientWidth || document.body.scrollWidth;"); int windowHeight = (Integer) engine.executeScript("document.documentElement.clientHeight || document.body.clientHeight || document.body.scrollHeight;"); System.out.printf("windowSize : width=%d, height=%d\n", windowWidth, windowHeight); // ドキュメントサイズの取得 int documentWidth = (Integer) engine.executeScript("document.documentElement.scrollWidth || document.body.scrollWidth;"); int documentHeight = (Integer) engine.executeScript("document.documentElement.scrollHeight || document.body.scrollHeight;"); System.out.printf("documentSize : width=%d, height=%d\n", documentWidth, documentHeight); // スクロール位置 int scrollWidth = (Integer) engine.executeScript("document.documentElement.scrollLeft || document.body.scrollLeft;"); int scrollHeight = (Integer) engine.executeScript("document.documentElement.scrollLeft || document.body.scrollLeft;"); System.out.printf("scrollPosition : width=%d, height=%d\n", scrollWidth, scrollHeight); System.out.println("-------------------------"); WebViewSize size = new WebViewSize(); size.windowWidth = windowWidth; size.windowHeight = windowHeight; size.documentWidth = documentWidth; size.documentHeight = documentHeight; size.scrollWidth = scrollWidth; size.scrollHeight = scrollHeight; return size; } public static class WebViewSize implements Serializable { public int windowWidth; public int windowHeight; public int documentWidth; public int documentHeight; public int scrollWidth; public int scrollHeight; } }
EL式をスタンドアローンで使用する
BeanValidation1.1でEL式が利用できるようになったので、他のライブラリでもメッセージ中にEL式が利用できる方法を調べて見た。
ようは、Servletコンテナなして、EL式を利用する方法を紹介します。
EL2.x
EL2.xを単独で利用するには、Glassfishが提供しているライブラリを利用します。
ただし、これだけではだめで、ELContext、ELResolverなど単独で利用できるように別途実装が必要になります。
実は、Hibernate Validator5.xのパッケージ「org.hibernate.validator.internal.engine.messageinterpolation.el」内のクラスがその実装になります。
【参考】
【準備】
EL式のライブラリとその実装用のライブラリを追加します。
<dependency> <groupId>javax.el</groupId> <artifactId>el-api</artifactId> <version>2.2</version> </dependency> <dependency> <groupId>org.glassfish</groupId> <artifactId>el-impl</artifactId> <version>2.2</version> </dependency>
【単独で利用できるように実装したもの】
Githubに参考にして作った実装を上げておきます。
EL3.0のELProcessorと似た使い方ができます。
ELProcessor elProc = new ELProcessor(); elProc.setVariable("currentDate", new Date()); elProc.setVariable("formatter", new FormatterWrapper(Locale.getDefault())); String eval = elProc.eval("formatter.format('%1$tY/%1$tm/%1$td%n', currentDate)", String.class); System.out.println(eval);
【注意点】
EL式中で呼び出すオブジェクトの中のメソッドは、オーバライドオーバロードしていると、区別つかないためラップなどして使用する。
'java.util.Formatter#formatter'は、FormatterWrapperクラスでラップして呼び出す必要がある。
EL3.x
EL3.xから、スタンドアローンで利用できるように、専用クラスELProcessor、StandartELContextクラスが追加になりました。
【参考】
【準備】
EL式のライブラリとその実装用のライブラリを追加します。
<dependency> <groupId>javax.el</groupId> <artifactId>javax.el-api</artifactId> <version>3.0.0</version> </dependency> <dependency> <groupId>org.glassfish</groupId> <artifactId>javax.el</artifactId> <version>3.0.0</version> </dependency>
【使い方】
ELProcessor elProc = new ELProcessor(); // 式中で利用する変数の追加 elProc.defineBean("a1", 5); elProc.defineBean("a2", 3); // 式の評価 int ret = elProc.eval("a1 + a2");
Hibernate Validator5.x(Bean Validatoin1.1)中のEL式
Hibernate Valiador5.xから、EL式が利用できるようになりました。
しかし、ラムダ式などのEL3.xで追加された機能は利用できません。
基本的にEL2.xの機能が利用できます。
Hibernate ValidatorのEL式を解釈するクラス「org.hibernate.validator.internal.engine.messageinterpolation.InterpolationTerm」を見ると、EL2.x系の使い方になっているためということがわかります。
EL3.0で追加された、ELProcessorを利用するように実装を切り替えれば、EL3.0が使えるようになります。
BeanValidationからMessageInterpolatorが独自のものが設定できるようになっているため、org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator中のメソッド「interpolateExpression」の中を変更すれば動作するようになると思います。
public class ResourceBundleMessageInterpolator implements MessageInterpolator private String interpolateExpression(TokenIterator tokenIterator, Context context, Locale locale) throws MessageDescriptorFormatException { while ( tokenIterator.hasMoreInterpolationTerms() ) { String term = tokenIterator.nextInterpolationTerm(); // EL式の処理部分。↓の処理を独自なものに変更する。 InterpolationTerm expression = new InterpolationTerm( term, locale ); String resolvedExpression = expression.interpolate( context ); tokenIterator.replaceCurrentInterpolationTerm( resolvedExpression ); } return tokenIterator.getInterpolatedMessage(); } }
XlsMapperのリリース
以前に作っていた、XLSBeansの拡張版を「XlsMapper」としてリリースしました。
書き込み機能の他、Validation機能を追加しました。
Validation機能は、Springの仕様を参考に作りましたが、途中からSpring依存でも良さそうな気がしてきました。
FieldProcessorのロジック部分、AnnotationReader部分はXLSBeansのコードが残っていますが、他は書き換えました。
リポジトリなどのURL
XLSBeansとの違い
下記のURLにまとめてあります。
http://mygreen.github.io/xlsmapper/diff_xlsbeans.html
【改善機能】
- アノテーションの名称を"@Xls〜"として、他のライブラリのアノテーションと区別がつきやすいようにしています。
- エラーメッセージの内容を詳細に表示し、設定間違いの対応をしやすくしています。
- Generics対応として、@XlsMapColums、@XlsIterateTablesにも対応しており、マッピング先のClassを指定する必要がありません。
- シートの座標位置の取得が、@XlsMapColumnを付与したフィールドにも対応しています。
- アノテーションを付与するフィールドの修飾子として、public以外のprivate/protectedにも対応しています。
【追加機能】
- 書き込み機能があります。
- @XlsHorizontalRecords/@XlsVerticalRecordsに書き込み用の属性を追加し、 行が余ったときや足りないときの動作をカスタマイズできます。
- 読み込み時、書き込み時のコールバック用メソッドを定義するためのアノテーション @XlsPreLoad/@XlsPostLoad/@XlsPreSave/@XlsPostLoadが使用できます。
- 型変換機能として、専用のアノテーションを利用して各フィールドごとに細かく設定できます。
- 読み込んだシートの内容をチェックするValidation機能があります。
- 独自にValidatorを実装する方式の他、Bean Validation 1.0/1.1に対応しています。
- セルの座標位置のように、マッピング対象の見出し名を取得するこができます。
【削除機能】
今後の予定
- マニュアルをもう少し詳しく書く。
- テスタを作成して、品質を上げる。
Velocityの簡易エディタを求めて
システム導入時などにVelocityのテンプレートをいじる機会があった。
EclipseとかのIDEだと重く、また、プロジェクト形式にする必要があるので、気軽には使えない。
ネットで探したり、Velocityの公式サイトにあるエディタ一覧を参考にして、「jEdit」にした。
jEditはJava製のエディタで、Windows、Linuxなどで動作が可能で、プラグインも充実しています。
しかし、プラグインを入れないと使い勝手が悪いといものもあります。
また、大学生時代の研究で、独自システムの専用言語のエディタとして、自分でカスタマイズして使っていたというのもあり、これにしました。
jEditは標準で、Velosityのシンタクス表示、コード補完も対応しています。。
しかし、マクロや変数などのアウトライン表示は、ctagsを使ったプラグイン「CtagsSideKicker」が必要です。
設定ファイルで簡易的なアウトライン表示にも対応可能です。
jEditのVelocityの設定「CtagsSideKicker(旧CodeBrowser)」
- 前提のプラグインに「ErrorList」が必要なので、先にインストールしておく。
- 先にErrorListをインストールしないとエラーが出る。
- 続いて、プラグインマネージャから「CtagsSideKicker」をインストールする。
- インストール後、「CtagsSideKicker」のプラグインオプションの項目「ctags Path」で「ctags.exe」のパスを指定する。
- 「ctags.exe」は、日本語対応版のサイトからダウンロードする。
- 環境変数「HOME」に、設定ファイル「.ctags」を次の内容で配置する。
--langdef=velocity --langmap=velocity:.vm --regex-velocity=/^#macro[ ]*\([ ]*([a-zA-Z0-9_]+)/#macro \1/m,macro/i --regex-velocity=/^#set[ ]*\([ ]*(\$[a-zA-Z0-9_]+)/#set \1/m,macro/i
- 設定ファイルを設置後、プラグイン「SideKick」のプラグインオプション「Parsers」で、モードに対するパーサとして「ctags」を指定する。
- モード「velocity」に対して、パーサ「ctags」を指定する。
注意事項として、設定ファイルいよるアウトラインは正規表現で定義するため、複数行にわたるコメント(#*〜*#)中に、定義対象の文字が見つかった場合は誤検出する。
誤検出を許したくない場合は、自分でctagsにパーサを追加する必要がある。
c言語で比較的、簡単にできるが、ctagsを再ビルドする必要がある。
自分で、ctagsを拡張したい場合は、次のサイトを参考にするとよい。
jEditのその他の設定
jEditの設定は、メニュー「ユーティリティ」−「グローバルオプション」から変更できる。
よく設定する値
- ラップマージン:「編集」の「ラップマージン」から設定する。100または120くらいにする。
- タブの表示幅:「編集」の「タブ幅」から設定する。値は、4にする。
- インデント幅:「編集」の「インデント幅」を4にする。
- エディタのフォント:「テキストエリア」の「テキストフォント」から設定可能。「MS ゴシック」など等幅かつ日本語対応のフォントにする。
- 現在のカーソル行の表示:「テキストエリア」の「行列マーカー」にチェックを入れる。
バックアップファイルを作りたくない場合
jEditは標準で、「sample.txt~」というように、バックアップファイルが作られる。
これを作りたくない場合は、メニュー「ユーティリティ」−「グローバルオプション」の「保存&バックアップ」から変更できる。
- 「保存&バックアップ」の項目「バックアップの最大数」を“1”→”(空)”にクリアする。
- 「保存&バックアップ」の項目「バックアップファイル名サフィクス」を“~”→”(空)”にクリアする。