JavaFX: WYSIWYGエディタを作る

この記事は2014年のJavaFX Advent Calendarの5日目の記事です。
前日の記事はmike_neckさんのJavaFXで画面を作るときにFXMLを小さく作るです。
明日の記事はKatsumi Kokuzawaさんです。

JavaFXでHTMLのWYSIWYGエディタを作れないかな、と考えてたら、そのものずばりHTMLEditorというクラスがあります。
そのチュートリアルサンプルコードをほんの少しカスタマイズして、既存のWebページを読み込んで、それをHTMLEditorで編集して、編集結果をブラウザ表示するというサンプルを作ってみました。

こんな感じ。
f:id:soutoku:20141202160550p:plain
左側のセピアのブラウザがオリジナルのWebページです(ブラウザ側でセピアトーン効果をかけています)。真ん中上段のHTMLEditorで編集可能で、「Load Content in Browser」ボタンを押すと、下段のテキストエリアに編集済みのHTMLコードと、右側のブラウザに編集結果のページが表示されます。

ブラウザからHTMLのソースを取得してHTMLEditorにセットする部分が地味に面倒です。一発で取得できないものだろうか。

玩具みたいなものですが、複雑なページの一部を編集した際のHTMLソースが欲しい場合などに使えそうです。

import java.io.StringWriter;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.concurrent.Worker;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextArea;
import javafx.scene.effect.SepiaTone;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.web.HTMLEditor;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;

public class HTMLEditorSample extends Application {

    @Override
    public void start(Stage stage) {
        stage.setTitle("HTMLEditor Sample");
        stage.setWidth(500);
        stage.setHeight(500);
        Scene scene = new Scene(new Group());
        
        // 横にレイアウトする為のHBoxを追加
        HBox base = new HBox();
        
        VBox root = new VBox();
        root.setPadding(new Insets(8, 8, 8, 8));
        root.setSpacing(5);
        root.setAlignment(Pos.BOTTOM_LEFT);
        
        final HTMLEditor htmlEditor = new HTMLEditor();
        htmlEditor.setStyle("-fx-font: 12 cambria; -fx-border-color: brown; -fx-border-style: dotted; -fx-border-width: 2;");
        htmlEditor.setPrefHeight(245);
        
        //HTMLのソースコードを表示するテキストエリアを追加
        final TextArea htmlCode = new TextArea();
        htmlCode.setWrapText(true);
        
        final WebView browser = new WebView();
        final WebEngine webEngine = browser.getEngine();
        
        //ブラウザにセピアトーン効果をかける
        browser.setEffect(new SepiaTone());
        
        ScrollPane scrollPane = new ScrollPane();
        scrollPane.getStyleClass().add("noborder-scroll-pane");
        scrollPane.setStyle("-fx-background-color: white");
        scrollPane.setContent(browser);
        scrollPane.setFitToWidth(true);
        scrollPane.setPrefHeight(180);
        
        //編集後のページを表示するWebViewを追加
        final WebView viewer = new WebView();
        final WebEngine engine = viewer.getEngine();
        
        Button showHTMLButton = new Button("Load Content in Browser");
        root.setAlignment(Pos.CENTER);
        showHTMLButton.setOnAction(arg0 -> {
            engine.loadContent(htmlEditor.getHtmlText());

            //テキストエリアにHTMLソースコードを表示
            htmlCode.setText(htmlEditor.getHtmlText());
        });
        
         // ページのロードが終了したときの処理。HTMLEditorにコンテンツを読み込む。
        Platform.runLater(() -> {
            webEngine.getLoadWorker().stateProperty().addListener((observableValue, oldValue, newValue) -> {
                if (newValue == Worker.State.SUCCEEDED) {

                    Document doc = webEngine.getDocument();
                    DOMSource source = new DOMSource(doc);
                    StringWriter result = new StringWriter();
                    TransformerFactory transFactory = TransformerFactory.newInstance();
                    try {
                        Transformer transformer = transFactory.newTransformer();
                        transformer.transform(source, new StreamResult(result));
                        htmlCode.setText(result.toString());
                        htmlEditor.setHtmlText(result.toString());
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
        });

        webEngine.load("http://soutoku.hatenablog.com/");

        root.getChildren().addAll(htmlEditor, showHTMLButton, htmlCode);
        //左からオリジナルページのブラウザ、エディタ、編集後のページのブラウザを並べる
        base.getChildren().addAll(browser, root, viewer);
        scene.setRoot(base);

        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }    
}

JavaFX: JavaFX8でブラウザ

さくらばさんの
JavaFX でブラウザ - JavaFX in the Box

の記事のコードを、勉強のため、とりあえずJavaFX8で動くところまで修正してみました。
本当はレイアウトはFXMLを使うべきなのだろうけれど、オリジナルのコードとの対比がわかるように(という言い訳で)そこは手を付けていません。

Reflectionの使い方が悪いのか、反射効果が反映されないので、代わりにセピアトーンのエフェクトを追加しています。

import javafx.application.Application;
import javafx.concurrent.Worker;
import javafx.concurrent.Worker.State;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.effect.Reflection;
import javafx.scene.effect.SepiaTone;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.text.Font;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;

public class WebViewDemo extends Application {

    private WebEngine engine;
    private TextField urlBox;

    @Override
    public void start(Stage stage) {
        stage.setTitle("WebView Demo");

        BorderPane borderPane = new BorderPane();
        borderPane.setLayoutY(10);
        borderPane.setLayoutX(10);
        Scene scene = new Scene(borderPane);
        stage.setScene(scene);

        // ブラウザ
        WebView view = new WebView();
        engine = view.getEngine();
        view.setPrefSize(600, 500);
        borderPane.setCenter(view);

        // 反射効果を加える
        Reflection reflection = new Reflection();
        reflection.setFraction(0.5);
        view.setEffect(reflection);
        // 反射効果が反映されないので、代わりにセピアトーン
        view.setEffect(new SepiaTone());

        // ページのロードが終了したときの処理
        Worker<Void> worker = engine.getLoadWorker();
        worker.stateProperty().addListener((ov, oldState, newState) -> {
            if (newState == State.SUCCEEDED) {
                String url = engine.getLocation();
                urlBox.setText(url);
            }
        });

        // 水平ボックス
        HBox hbox = new HBox(10);
        hbox.setPrefHeight(40);
        hbox.setAlignment(Pos.BASELINE_CENTER);
        borderPane.setTop(hbox);

        // テキスト入力
        urlBox = new TextField();
        urlBox.setFont(new Font("sanserif", 16));
        hbox.getChildren().add(urlBox);

        urlBox.setOnAction(event -> loadUrl());

        // ボタン
        Button button = new Button("Open");
        button.setFont(new Font("sanserif", 16));
        hbox.getChildren().add(button);

        button.setOnAction(event -> loadUrl());

        stage.show();
    }

    // ページのロード
    private void loadUrl() {
        String url = urlBox.getText();
        if (url != null && !url.trim().isEmpty()) {
            engine.load(url);
        }
    }

    public static void main(String[] args) {
        launch(args);
    }

}

こんな感じ
f:id:soutoku:20141202143700p:plain

Clojure: LeiningenのWindowsインストール

LeiningenをWindows 7 SP1に導入した際の手順を記録します。
インストーラを実行しただけではきちんと導入できない為

0. JDKは予めインストールしておきます。
 私の環境では、C:\Program Files\Java\jdk1.7.0_67とC:\Program Files\Java\jdk1.7.0_72にインストールされていますが、パスが通っているのは後者です。

1. leiningen-installer-1.0.exeをleiningen-win-installerサイトからダウンロードして実行します。
 私のようにJDKが二種類インストールされている場合は、途中でどちらを使用するか選択を求められますが、それ以外はデフォルト値のままインストールします。

2. スタートメニューに[Leiningen]フォルダが出来ています。その中の[Clojure REPL]を選択して実行すると、REPLが起動!しません。あれ?

3. コマンドプロンプトを立ち上げて

> lein repl

 と打ち込むと、

C:\Users\foo\.lein\self-installs\leiningen-2.5.0-standalone.jar can not be found.
You can try running "lein self-install"
or change LEIN_JAR environment variable
or edit lein.bat to set appropriate LEIN_JAR path.

 と表示されます。

> dir .lein\self-installs

 で該当ディレクトリの中身を確認すると、確かに空です。
 あ、そうか、自分で

> lein self-install

 とやらなくてはならないのですね。

4. leiningen-2.5.0-standalone.jarのダウンロードのために、

> lein self-install

 とやると今度は

Downloading Leiningen now...
'wget' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。

Failed to download https://github.com/technomancy/leiningen/releases/download/2.5.0/leiningen-2.5.0-standalone.jar

 と表示されます。私の環境では確かにwgetは入れていませんが、その場合はcurlを使ってインストールしてくれる筈ですし、.lein\bin\curl.exeはインストーラが導入してくれています。
 しょうがないので、
 .lein\bin\lein.batをメモ帳で開いて、以下の行をコメントアウトします。

::call wget >nul 2>&1
::if NOT ERRORLEVEL 9009 (
::    call wget --no-check-certificate -O %1 %2
::    goto EOF
::)

 再度

> lein self-install

を実行すると、

Downloading Leiningen now...
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   406    0   406    0     0    530      0 --:--:-- --:--:-- --:--:--   703
100 14.2M  100 14.2M    0     0  1509k      0  0:00:09  0:00:09 --:--:-- 2269k
        1 個のファイルを移動しました。

と実行され、無事に.lein\self-installs\leiningen-2.5.0-standalone.jarが導入されました。

5. REPLを起動するには、コマンドプロンプトから

> lein repl

とやるか、スタートメニューから[Leiningen]->[Clojure REPL]を実行します。

Clojure: はじめてのClojureを読んで

はじめてのClojure (登尾 徳誠 著、工学社 刊)を読みました。
2~3日で読める分量に、開発環境構築から、Webアプリケーション開発のチュートリアルまでを網羅しており、入門者がClojureを始める取っ掛かりとして最適な書籍になっていると思います。
だからこそ、つまらない誤植が多いのは少し残念なところです。正誤表はこちらにあります。

この本でClojure開発の雰囲気を学んだ後にプログラミングClojureで文法を学んで、おいしいClojure入門ツールや応用を学ぶというスキームが整ったことで、日本でのClojure開発の裾野が広がるといいですね。

GlassFish: Java EE 7 入門ではまったこと

Java EE 7 入門 NetBeansで始めるJava EE 7 First Tutorial

を実装していてはまった箇所を記録します。
※環境はNetBeans 8.0.1+GlassFish Server 4.0

P.70のユニットテストの実行は失敗します。

重大: Exception while preparing the app : Invalid resource : jdbc/tutorial__pm

恐らく、テスト・ライブラリに含まれているGlassFish Server 4 (埋込み可能コンテナ)にデータソースの設定をしてやる必要があるのだと思いますが、ここは解決せずにスルーしました。

P.97のlist.xhtmlファイルの実行で、やはりエラーが発生します。

重大:   Exception while invoking class org.glassfish.persistence.jpa.JPADeployer prepare method
重大:   java.lang.RuntimeException: Invalid resource : jdbc/tutorial__pm
重大:   Exception while preparing the app : Invalid resource : jdbc/tutorial__pm

もちろんglassfish-resources.xmlにはjdbc/tutorialの定義はされています。
対応策としては、GlassFish Serverのドメイン管理コンソールから「JDBC接続プール」と「JDBCリソース」を登録すること。
f:id:soutoku:20141008114329p:plain
f:id:soutoku:20141008134228p:plain
Windows7の場合、下記のファイルに設定が追加されるので、うまく行かない場合はこのファイルを直接書き換えてもよい。
※ファイルの編集はバックアップを取ってから行うこと。
C:\Users\ユーザ名\AppData\Roaming\NetBeans\8.0\config\GF_4.0\domain1\config\domain.xml
設定値は以下のとおり。

    <jdbc-connection-pool datasource-classname="org.apache.derby.jdbc.ClientDataSource" res-type="javax.sql.DataSource" name="TutorialPool">
      <property name="User" value="tutorial"></property>
      <property name="DatabaseName" value="tutorial"></property>
      <property name="Password" value="tutorial"></property>
      <property name="ServerName" value="localhost"></property>
      <property name="PortNumber" value="1527"></property>
      <property name="URL" value="jdbc:dervy://localhost:1527/tutorial"></property>
    </jdbc-connection-pool>
    <jdbc-resource pool-name="TutorialPool" jndi-name="jdbc/tutorial"></jdbc-resource>


P.154の「存在しないtodoidを指定してリクエストを送信」して「404 Not Found」エラーを表示させる処理は、チュートリアルのコードのままでは「500 Internal Server Error」になります。
原因は、ErrorModel.javaに引数無しのコンストラクタが存在しない為です。

Exception Description: The class todo.app.common.exception.ErrorModel requires a zero argument constructor or a specified factory method.  Note that non-static inner classes do not have zero argument constructors and are not supported.

オリジナルのコードを以下のように書き換えると想定の動作になります。
ErrorModel.java

public class ErrorModel {
    //errorMessagesをインスタンス生成後設定可能なように、final属性を除去
    //private final List<String> errorMessages;
    private List<String> errorMessages;
    
    //デフォルトコンストラクタを追加
    public ErrorModel() {
        this.errorMessages = null;
    }
    
    public ErrorModel(List<String> errorMessages) {
        this.errorMessages = errorMessages;
    }
    
    public List<String> getErrorMessages() {
        return errorMessages;
    }
    
    //セッターメソッドを追加
    public void setErrorMessages(List<String> errorMessages) {
        this.errorMessages = errorMessages;
    }
}

ResourceNotFoundExceptionMapper.java

@Provider
public class ResourceNotFoundExceptionMapper implements ExceptionMapper<ResourceNotFoundException> {
    
    @Override
    public Response toResponse(ResourceNotFoundException exception) {

        //ErrorModelをデフォルトコンストラクタで生成するように変更
        //ErrorModel errorModel = new ErrorModel(Arrays.asList(exception.getMessage()));
        ErrorModel errorModel = new ErrorModel();
        errorModel.setErrorMessages(Arrays.asList(exception.getMessage()));
        
        return Response.status(Response.Status.NOT_FOUND).entity(errorModel).build();
    }
}

※随時追加予定

NetBeans: Serving Web Content with Spring MVCチュートリアルの実装

Serving Web Content with Spring MVCチュートリアルを、NetBeans 8.0.1を使って実装する手順

1. プロジェクトの作成
 [ファイル]→[新規プロジェクト]でプロジェクトを作成する。その際、カテゴリは[Maven]、プロジェクトは[Javaアプリケーション]を選択する。(Spring Bootのサンプルなので、ここで[Webアプリケーション]を選択すると面倒くさいことになる)
f:id:soutoku:20140930181925p:plain
 プロジェクト名は適当に[hello]とかにしておく。するとプロジェクトウィンドウの初期状態は下図のようになる。
f:id:soutoku:20140930182336p:plain

2. pom.xmlの編集
 チュートリアルページの「▼Build With Maven」にある「pom.xml」の内容でpom.xmlを書き換える。
 すると、プロジェクトウィンドウの内容が下図のように書き換わる。
f:id:soutoku:20140930182959p:plain

3. 2つのクラスと1つのHTMLファイルの作成
 デフォルトで作成されるcom.mycompany.helloパッケージは削除する。
 チュートリアルページの「Create a web controller」「Make the application executable」に従ってGreetingController.java、Application.java、greeting.htmlをそれぞれ実装する。
 プロジェクトウィンドウの内容は下図のようになっている筈である。
f:id:soutoku:20140930183453p:plain

4. ビルドと実行
 後はビルドして実行すれば、初回のみメイン・クラスの選択ウィンドウが表示された後、出力ウィンドウに下図のようなSpring Bootのメッセージが表示されアプリケーションが実行される筈である。
f:id:soutoku:20140930184235p:plain
 dispatcher-servlet.xmlとかapplicationContext.xmlを書かなくていいのは新鮮。

5. サイトアクセス
下記のようにブラウザからアクセスすると

http://localhost:8080/greeting
http://localhost:8080/greeting?name=User
http://localhost:8080/greeting?name=名前

「Hello, World!」とか「Hello, User!」とか「Hello, 名前!」と表示される筈である。

GlassFish: postパラメータの文字化け

NetBeans Docs & SupportのSpring Web MVC入門を実装してみたところ、日本語で入力した場合、パラメータのみが文字化けする。
(helloServiceのメッセージ文やJSPに日本語を記述しても文字化けはしない)

回避するにはWEB-INF/sun-web.xmlに、以下の定義を追加する。

<parameter-encoding default-charset="UTF-8" />

確認環境:Java 1.7.0
     NetBeans 8.0
     GlassFish Server 4
     Spring Framework 3.2.7