20200104のJavaに関する記事は8件です。

JDK 14でPreview ReleaseされるRecordsが素敵なのでJShellで試そう

はじめに

DZoneで JDK 14 Rampdown: Build 27 を読むと

One might say that JDK 14 Early Access Build 27 is the "records build."

と書いており、とうとう試せるようになった Records をJShellで試してみました。

なお、 JDK14 のリリース時や、その後の Records の正式リリース時に内容は変わっているかもしれませんので、ご容赦ください。

JEP 359: Records

JEP 359: Records1 はJavaの文法改善を目的としたProject Amberの1つ 2 です。

半年ごとにリリースされるようになった Java ですが、当初のもくろみどおり、いろいろモダンな言語も参考に改善が進んでいます。

そんな中で Records は、TypeScriptの Constructor Shorthand や、Kotrinの Primary Constructor のコンストラクタ引数がフィールド変数の定義を兼ねる簡単な記法2のように、 レコード名と引数(レコードコンポーネント)の定義で、データの入れ物となるデータクラスを定義する仕組みです3

これが使えるようになると、戻り値、DTO(Data Transfer Objects)、Stream APIの操作、etc...などで「ただの"データの入れ物"を作るときにも、クラス(や適切なフィールド・メソッド)を作らないといけない」という現状に、「データの入れ物として明示的にデータクラスを作れば良い」という選択肢が加わります。

Recordsの使い方

基本的な使い方

Recordsの記法は、Qiitaでも "Amberで検討されているJava構文の変更 " や "Kotlinと今後のJavaはどっちがいい?" で解説されていますが、

record Person(String name) {};

と書くと、

final class Person {

  // フィールド変数
  public final String name;

  // 引数つきコンストラクタ
  public Point(String name) {
    this.name = name;
  }

  // フィールド変数にあわせて Getter, hashCode, equals, toString が自動実装される
  // getterはfluentな(フィールド変数名と同じ)メソッド名。
  public String name() {
    return this.name;
  }
  public int hashCode() { /* 中略、等価性を保つように実装 */ }
  public boolean equals() { /* 中略、等価性を保つように実装 */ }
  public String toString() { /* 中略、クラス名・フィールド名・値を返す */ }

  // 以上
}

という、イミュータブルなクラスと同等のデータクラスが自動的に定義されたことになります。便利。

ただし、これは単なるクラスの速記法や、lombokの様なボイラープレートを解決する記法ではなくて4、「データの入れ物」という新しい仕組み(enumの様な制限のあるクラスの仕組み)を作ることが目的で5、冗長性の解消はあくまで結果であるとJEPにも書いてあります。

実際に使うときには、このあたりを意識した方がよさそうです。

インターフェースの宣言

classと同様に、 implements を使います。

例えば、Serializable インターフェースを定義したいときは、

record Person(String name) implements Serializable {};

と書けば

final class Person implements Serializable {
 // 略
}

とシリアライズ可能なデータクラス定義されたことになります。

継承

スーパークラスになることも、サブクラスになることもできません。先述のとおり、あくまで従来のクラスの速記法ではないというスタンスです。

独自のコンストラクタやメソッドを増やす

自動的に定義されるコンストラクタやメソッド以外を増やしたい場合は、ボディ({}の中)に記載します。

record Person(String name){
  Person() {
    this("Jhon Doe");
  }
  public int getNameLength() {
    return name.length();
  }
} 

上記の例であれば、recordで自動定義される内容に加えて、引数なしコンストラクタ、getNameLengthメソッドが用意されます。

増やせるものは、コンストラクタ、クラスメソッド、クラスフィールド、クラスイニシャライザ、インスタンスメソッド、インスタンスイニシャライザです。データクラスの形を変えることがないよう、インスタンスフィールドは追加できません6

JShellで使ってみる

JShellで使ってみます。試したPCには、執筆時にSDKMAN!でインストールできる JDK 14 Build 28 (14.ea.28-open) が入っています。

java -version
openjdk version "14-ea" 2020-03-17
OpenJDK Runtime Environment (build 14-ea+28-1366)
OpenJDK 64-Bit Server VM (build 14-ea+28-1366, mixed mode, sharing)

以下、実際に再現してみたい方は jshell> とか ...> は除いてコピペしてみてください。

1. jshellの起動

プレビュー版の機能を有効にして、JShellを起動します。

jshell --enable-preview

2. record でデータクラスを定義してインスタンス化する

String name をレコードコンポーネント(つまり、インスタンスフィールド)に持つ Person データクラスを定義してみます。

jshell> record Person(String name) {} ;

特に表示無く、データクラスが完成するので、インスタンス化してみます。

jshell> var someone = new Person("Yamada");
someone ==> Person[name=Yamada]

someone変数でインスタンスが参照できるようになりました。

データを取り出したり、メソッドを使ってみましょう。

jshell> System.out.println(someone.name());
Yamada
jshell> System.out.println(someone.toString());
Person[name=Yamada]

等価性も確認してみましょう。

var other = new Person("Yamada");
   ...> someone.equals(other);
other ==> Person[name=Yamada]
$7 ==> true
jshell> other = new Person("Ichikawa");
   ...> someone.equals(other);
other ==> Person[name=Ichikawa]
$9 ==> false

インスタンスが異なっても、フィールドの値の等価性が判定できていますね。

3. インターフェースを設定してみる

Personと、SerializableなPersonを作って、インターフェース型を判定してみましょう。

someone instanceof Serializable;
|  エラー:
|  不適合な型: Personをjava.io.Serializableに変換できません:
|  someone instanceof Serializable;
|  ^-----^
jshell> record SerializablePerson(String name) implements Serializable {} ;
   ...> var nextOne = new SerializablePerson("Sato");
   ...> nextOne instanceof Serializable;
nextOne ==> SerializablePerson[name=Sato]
$16 ==> true

implements Serializable している SerializablePerson のインスタンスは、 instanceof の結果が true になっており、インターフェースの設定が効いていることがわかります。

4. 独自のコンストラクタやメソッドを増やす

Personのボディでデフォルトコンストラクタを増やしたり、レコードコンポーネントを使うメソッドを追加してみましょう。

jshell> record Person(String name) {
   ...>         Person() {
   ...>             this("Jhon Doe");
   ...>         }
   ...>         public int getNameLength() {
   ...>             return name.length();
   ...>         }
   ...>     }

Personがデフォルトコンストラクタでインスタンス化されると、Jhon Doe で初期化されるようにしています。また、lengthを返すインスタンスメソッドも追加しています。

インスタンス化して使ってみましょう。

jshell> var someone = new Person();
   ...> someone.name();
   ...> someone.getNameLength();
someone ==> Person[name=Jhon Doe]
$3 ==> "Jhon Doe"
$4 ==> 8

デフォルトコンストラクタの設定や、増やしたメソッドが動作していますね。

5. その他

Class.isRecord()

record から作られたデータクラスかどうかは、 isRecord で調べられます。

jshell> Person.class.isRecord();
$8 ==> true
jshell> String.class.isRecord();
$9 ==> false

Class.getRecordComponents()

record から作られたデータクラスが持つレコードコンポーネントを、 RecordComponent の配列で返します。

jshell> record Range(int lo, int hi) {};

jshell> Range.class.getRecordComponents();
$12 ==> RecordComponent[2] { int lo, int hi }

6. 利用例

利用例として、JEPの背景文章にあった、Streamの中間操作の例を試してみました。

record Person(String name) {} ;

record PersonX(Person p, int hash) {
  PersonX(Person p) {
    this(p, p.name().toUpperCase().hashCode());
  }
}

//本来はどこかのデータソースから取り出してくる想定と思います
var list = List.of(new Person("Yamada"), 
  new Person("Ichikawa"), 
  new Person("Sato"), 
  new Person("Tanaka"));

list.stream()
  .map(PersonX::new)
  .sorted(Comparator.comparingInt(PersonX::hash))
  .peek(System.out::println)
  .map(PersonX::person)
  .collect(Collectors.toList());

JShellの結果はこちら。

list ==> [Person[name=Yamada], Person[name=Ichikawa], Pers ... ato], Person[name=Tanaka]]
PersonX[person=Person[name=Tanaka], hash=-1827701194]
PersonX[person=Person[name=Yamada], hash=-1684585447]
PersonX[person=Person[name=Ichikawa], hash=-159644485]
PersonX[person=Person[name=Sato], hash=2537801]
$16 ==> [Person[name=Tanaka], Person[name=Yamada], Person[name=Ichikawa], Person[name=Sato]]

若干、実行結果がJShellで省略されていますが、Yamada, Ichikawa, Sato, Tanakaを持つ(データ)クラスのリストを、PersonXに変換、hash順にソートする仕組みです7

Person、PersonX は record で作られたデータクラスですが、通常のクラスとは異なるもののクラスの枠組みは外れていないので、利用時も通常のクラスと混ぜて使ったり、これまでの文法を大きく変えずに利用できます

旧来であれば、Person, PersonXはそれぞれクラス定義をして、ミュータブル/イミュータブルにあわせてメソッドを用意したり... という手はずが必要でしたが、「データの入れ物」として扱えるのなら、

record Person(String name) {} ;
record PersonX(Person p, int hash) {
  PersonX(Person p) {
    this(p, p.name().toUpperCase().hashCode());
  }
}

だけで記述が済みます。

また、クラスなのか「データの入れ物」なのかの区別が必要な時も、Class#isRecord で判断できます。

このように Records を使うことで、「(単なる)データの入れ物」と従来のクラスの区別の仕方をJavaに取り入れて、かつ記述が簡潔に済む ようになりそうです。

おわりに

JShellを使って、JDK14からプレビューリリースされる Records を、JShellで体験してみました。

私も正直、最初は「ボイラープレート対策!素敵!」と思ってたんですが、JEPの文章を読んでみたり実際に使ってみると、文中にあった「enumの時と同じように現れた、新しい "データの入れ物" という仕組み」という側面の方がしっくりくるようになりました。

もちろん、Kotlinの data class をさっさと混在して使ってしまう、という手もありますが、JEP 359は、Javaが標準でも次第に使いやすくなっていく象徴の様にも思えます。

JDK14、そしてその後の正式リリースが待ち通しいです。


  1. 英語が苦手な方は kagamihogeの日記 - JEP 359: Records (Preview)をテキトーに訳した で概要をつかみましょう。 

  2. Amberの具体的な内容は きしだなおきさんの Amberで検討されているJava構文の変更 を読むとわかりやすいです。 

  3. JEPの背景文章にはKotlinのdata class、Scalaの case class、C#のrecord classがデータクラスの例にあげられていました。 

  4. JEP 359: Recordsより、
    "While it is superficially tempting to treat records as primarily being about boilerplate reduction, we instead choose a more semantic goal: modeling data as data. (If the semantics are right, the boilerplate will take care of itself.)"  

  5. JEP 359: Recordsより、
    Records are a new kind of type declaration in the Java language. Like an enum, a record is a restricted form of class. 

  6. JEP 359: Recordsより、
    "The record's body may declare static methods, static fields, static initializers, constructors, instance methods, instance initializers, and nested types."  

  7. 元のコードにあったTypoを修正し、わかりやすい用にlimitを外したり、peekを付けたりしています。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS Cognitoで認証したユーザのID トークンを Java で検証する

やりたいこと

AWS Amplify の Authentication で認証したクライアントが id_tokenをサーバに送信し、Javaで実装したサーバで id_token を検証します。

TL;DR

  • 発行者(ペイロードのiss) が対象にしている Cognitoのユーザプールであることを確認する。
  • 署名の検証は https://github.com/auth0/java-jwt に丸投げ。
  • com.auth0.jwt.JWTVerifier はスレッドセーフなのを前提にして、Cognitoが署名に使用した RSA鍵ペアを取得する回数を減らすために自前でキャッシュする。

実装

IdTokenValidator.java
IdTokenValidator.java
package com.exampleawsCognito.jwt;

import java.net.MalformedURLException;
import java.net.URL;
import java.security.interfaces.RSAPublicKey;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.GregorianCalendar;

import com.auth0.jwk.GuavaCachedJwkProvider;
import com.auth0.jwk.Jwk;
import com.auth0.jwk.JwkException;
import com.auth0.jwk.UrlJwkProvider;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;


/**
 * Cognitoで認証して得られるid tokenを扱う
 *
 * <p>
 * 実装の参考にしたソース
 * https://github.com/awslabs/cognito-proxy-rest-service/blob/master/src/main/kotlin/com/budilov/cognito/services/CognitoService.kt
 * </p>
 *
 * @ThreadSafe
 *
 */
public class IdTokenValidator {
    private static final String AWS_REGION = "ap-northeast-1";
    private static final String AWS_COGNITO_USER_POOL_ID = "my_userpool_id";

    private static JWTVerifier verifier = null;
    private static long verifierExpire = -1;
    private static final long VERIFIER_LIVE_MILISEC = 10 * 60 * 1000; //10分
    private static final JWT JWT = new JWT();

    /**
     * コンストラクタ
     */
    public IdTokenValidator() {
    }

    /**
     * IDトークン を検証します
     *
     * @param idToken 検証対象のIDトークン
     * @return 検証に成功した場合は ID Tokenのペイロード
     *
     * @throws InvalidTokenException ID Tokenの値が不正なので認証に失敗した
     */
    public DecodedJWT verify(String idToken) throws InvalidTokenException {
        DecodedJWT decodedToken = JWT.decodeJwt(idToken);

        // cognitoのユーザプールで署名された事を確認する
        String iss = decodedToken.getIssuer();
        if (!jwtTokenIssuer().equals(iss)) {
            throw new InvalidTokenException("ID トークンの発行者が対象のシステムではありません。iss=" + iss, idToken);
        }

        //  ID トークンの用途が「ID」であることを確認します。
        String tokenUse = decodedToken.getClaim("token_use").asString();
        if (!"id".equals(tokenUse)) {
            throw new InvalidTokenException("ID トークンの用途が ID ではありません。token_use=" + tokenUse, idToken);
        }

        // 署名のアルゴリズムを確認します。
        String alg = decodedToken.getAlgorithm();
        if (!"RS256".equals(decodedToken.getAlgorithm())) {
            throw new InvalidTokenException("ID トークンの署名アルゴリズムが対応していないものです。alg =" + alg, idToken);
        }

        // payloadと署名を検証します。
        DecodedJWT decodedJWT = null;
        if ((decodedJWT = tokenVerify(decodedToken)) == null) {
            throw new InvalidTokenException("ID Tokenの検証に失敗しました。", idToken);
        }

        return decodedJWT;
    }

    /**
     * auth0の のライブラリを利用して 検証を行う
     *
     * @param kid ID トークンのヘッダーにある キーID
     * @return nullでなければ デコードされた ID トークン
     *
     * @throws InvalidTokenException 検証に失敗した
     */
    private DecodedJWT tokenVerify(DecodedJWT jwToken) throws InvalidTokenException {

        try {
            DecodedJWT verified = getVerifier(jwToken.getKeyId()).verify(jwToken);
            return verified;
        } catch (Exception e) {
            throw new InvalidTokenException(e);
        }
    }

    /**
     * JWTVerifier のインスタンスを取得する。
     *
     * <p>
     * JWTVerifier は ver.3 からスレッドセーフになったので再利用する。
     * ただし、署名に使われた RSAキーペアが更新される可能性を考えて、定期的に更新する
     * </p>
     *
     * @param kid 署名に使われたキーID
     *
     * @return
     *
     * @throws MalformedURLException
     * @throws JwkException
     */
    private JWTVerifier getVerifier(String kid) throws MalformedURLException, JwkException {
        if (verifier != null && System.currentTimeMillis() < verifierExpire) {
            // 有効期限内ならそのまま使う
            return verifier;
        }
        synchronized (JWT) {
            // ロックを獲得したので念のためにもう一度確認してからインスタンスを生成する
            if (verifier != null && System.currentTimeMillis() < verifierExpire) {
                return verifier;
            }

            UrlJwkProvider http = new UrlJwkProvider(new URL(jwksUrl()));
            GuavaCachedJwkProvider provider = new GuavaCachedJwkProvider(http);
            Jwk jwk = provider.get(kid);

            Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) jwk.getPublicKey(), null);
            verifier = JWT.require(algorithm)
                    .withIssuer(jwtTokenIssuer())
                    .build();

            // JWTVerifier の寿命を延ばす
            verifierExpire = System.currentTimeMillis() + VERIFIER_LIVE_MILISEC;

            Calendar expire = GregorianCalendar.getInstance();
            expire.setTimeInMillis(verifierExpire);
            Logger.info("JWTVerifierのインスタンスを生成しました。期限は"
                    + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(expire.getTime()));

        }

        return verifier;

    }

    /**
     * ID トークンの発行者を取得します
     *
     * @return
     */
    private String jwtTokenIssuer() {
        return String.format("https://cognito-idp.%s.amazonaws.com/%s", AWS_REGION, AWS_COGNITO_USER_POOL_ID);
    }

    /**
     * JSON Web トークン (JWT) セットのURLを取得します。
     *
     * @return
     */
    private String jwksUrl() {
        return jwtTokenIssuer() + "/.well-known/jwks.json";
    }

}

InvalidTokenException.java
InvalidTokenException.class
public class InvalidTokenException extends Exception {

    public InvalidTokenException(String message, String idToken) {
        super(message + " token is " + idToken);
    }

    public InvalidTokenException(Throwable e) {
        super(e);
    }
}

使う側
    try{
        // TODO IdTokenValidator はスレッドセーフなので static にする
        DecodedJWT payload = new IdTokenValidator().verify(idToken);
        String cognitoUserName = payload.getClaim("cognito:username").asString();

        // 必要な処理

    }catch (InvalidTokenException e) {
        Logger.error("ID トークンの検証に失敗しました", e);
        badRequest("IdTokenの値が不正ですよ");
    }

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Windows上のOpenJDKな環境でJava Web Startを使う

:information_source: この記事は別サイトで執筆したWindows上のOpenJDKな環境でJava Web Startを使う – ひつじぶどうの内容を転載しています。

はじめに

タイトルの通り, OpenJDKな環境でJava Web Startを使いたくなった(外発的動機)のでその時のめもです。

Oracleに頼ることなくJava Web Startを使うにはIcedTea-Webというのを使えば良いみたいですが, Linux環境で使う記事はたくさんあったもののWindows環境で使う記事はあんまり見かけなかったのでまとめました。

実行環境

項目
OS Windows 10 Pro
Build 1809
JDK Version OpenJDK 1.12

導入手順

下記の手順を実施します。

OpenJDK 12を準備する

そもそもJava Web StartをOpenJDKで使いたいと思う人のPCにはOpenJDKが入っているとは思いますが, 念のため。
12を選んだのは単に記事執筆時点での最新版だったからです。

OpenJDKのダウンロードページからOpenJDK 12をダウンロードし, zipファイルを解凍する
スクリーンショット 2019-05-18 18.57.01.png

② 解凍されたフォルダ(jdk-12.0.1)を適当な場所に配置する(今回はC:\直下に配置しました)
③ 「コントロールパネル」>「システム」>「システムの詳細設定」を開く
スクリーンショット 2019-05-18 19.02.32.png

④ 「詳細設定」タブの「環境変数」からユーザー環境変数"Path"を編集し, C:\jdk-12.0.1\bin (②で配置したパスを指定してください)を追加する
スクリーンショット 2019-05-18 18.57.58.png

IcedTea-Webを準備する

Wikipedia先生によると, IcedTeaとは2007年6月にRedHatが開始したOpenJDK用のビルドおよびシステム統合プロジェクト, ということらしいです。
IcedTeaプロジェクトのIcedTea-Webというものを使ってJava Web Startの機能を利用できるようにします。

① IcedTeaのダウンロードページに行く

icedtea-web-binaries/1.8/windows/ に進む(他のバージョンが良い人は1.8を他バージョンに適宜読み替えてください)

③ itw-installer.msi をダウンロードする
スクリーンショット 2019-05-18 19.17.00.png

④ itw-installer.msiを実行し, ウィザードに沿ってインストールする(特に初期設定から設定値は変えていません)
スクリーンショット 2019-05-18 19.19.30.png

⑤ インストールが終わったら, C:\Program Files\IcedTeaWeb\WebStart\bin をユーザー環境変数"Path"に追加する
スクリーンショット 2019-05-18 19.23.25.png

実行

実行するにはPowerShell等で下記のようにjavaws.exeの引数としてjnlpファイルのパスを渡してあげればOKです。

PS> javaws.exe TEST.jnlp

C:\Program Files\IcedTeaWeb\WebStart\bin\javaws.exe をjnlpファイルに関連付けてあげるとjnlpファイルをダブルクリックしたりするだけで実行できるので便利かもしれません。
スクリーンショット 2019-05-18 19.25.57.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaとJacksonでJSON その③ HTMLにJSONを埋め込んでJavaScript から利用する

はじめに

JSONを使う場合、JavaScriptからAjax経由でデータの送受信を行うケースが多いと思う。しかしながら、サーバからHTMLを受信したタイミングで、JSONデータを受け取ってJavaScriptで利用したいケースもある。この場合、サーバから返却するHTMLの中にJSONデータを埋め込んで、それをJavaScriptのオブジェクトとして読み込むことになる。PHPを利用した場合は、HTML に JSON データを埋め込んで JavaScript から利用するに記載の事例があったが、我らがJava(Servlet/JSP)による事例がなかったため、悪戦苦闘した結果をここに残しておく。

環境

  • Java 1.8
  • Tomcat 8.0.53
  • Jackson 2.10.1

まずは何も考えずにやってみよう⇒失敗

サーバ側

サーバ側は以下の通りとした。"<"や">"については、前回同様、HTMLのタグとして解釈される恐れがあることから、Unicodeエスケープシーケンス変換はそのままとしている。前回までとの違いは、JSON文字列を、HttpRequestのパラメータとして保存し、それをJSPに処理させている点だ。詳しくはクライアント側の方で解説する。
また、意地悪データとして、"Programmer"⇒"Programmer\"のようにデータの最後に\を入れてみた。

ServletTest2.java
package servletTest;

import java.io.IOException;
import java.util.List;
import java.util.ArrayList;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletContext;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

@WebServlet("/helloworld2")
public class ServletTest2 extends HttpServlet {
    private static final long serialVersionUID = 1L;
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        //Javaオブジェクトに値をセット
        JsonBean jsonBean = new JsonBean();
        jsonBean.setId(1);
        jsonBean.setName("kimisyo");
        List<String> datas = new ArrayList<>();
        datas.add("Programmer\\");
        datas.add("Data Scientist<script>alert('hello!')</script>");
        jsonBean.setDatas(datas);

        ObjectMapper mapper = new ObjectMapper();
        mapper.getFactory().setCharacterEscapes(new CustomCharacterEscapes());

        //JavaオブジェクトからJSONに変換
        String testJson = mapper.writeValueAsString(jsonBean);

        //JSON文字列をrequestにセット
        request.setAttribute("jsonStr", testJson);
        ServletContext sc = getServletContext();
        sc.getRequestDispatcher("/clientTest2.jsp").forward(request, response);

    }
}

クライアント側

クライアント側はjspに処理を記載している。HttpRequestのパラメータとして設定されたJSON文字列を一旦Javaの変数に保存し、それをJavaScriptの中のJSONのParseの引数に設定している。
これがうまくいけば、dataというオブジェクトを通して、JSONのハンドリングが可能となる。

clientTest2.jsp
<%@ page contentType="text/html; charset=UTF-8"%>
<%
    String jsonStr = (String)request.getAttribute("jsonStr");
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World</title>
<script>
    var data = JSON.parse('<%=jsonStr%>');
     alert(data.datas);
</script>
</head>
<body>
</body>
</html>

失敗状況

上のServletを実行してみると、JSON.parseのところでSCRIPT1014: 文字が正しくありません。というJavaScriptのエラーがでる。

最終的にブラウザに出力されたHTMLは以下の通りだ。
JSONに入れた\は\にちゃんとエスケープされているし、"<"や">"はUnicodeエスケープシーケンスに変換されている。一見問題なさそうに見える。

さぁ、何がまずかったのか考えて見よう。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World</title>
<script>
    var data = JSON.parse('{"id":1,"name":"kimisyo","datas":["Programmer\\","Data Scientist\u003Cscript\u003Ealert(\u0027hello!\u0027)\u003C\u002Fscript\u003E"]}');
    alert(data.datas);
</script>
</head>
<body>
    <div id="test"></div>
</body>
</html>

失敗原因

失敗原因はJavaScript文字列のエスケープ漏れだ。まず、JavaScriptでは、\はエスケープ用の文字として使われる。このため、"Programmer\\"はJavaScriptの文字列として"Programmer\"と認識される。これがJSON.Parseに引き渡されると、JSONでも"\"の文字自体は、"\"のようにエスケープしなければならないため、不正なJSONデータとして扱われるのだ。

対策

対策としては、JavaScript用のエスケープ処理をかました上でJSON.parseの引数に与えればよい。修正ソースを以下に記載しておく。ここではJavaScript文字列のエスケープとして、"\"と"'"をエスケープするための処理を入れている。

clientTest2.jsp
<%@ page contentType="text/html; charset=UTF-8"%>
<%
    String jsonStr = (String)request.getAttribute("jsonStr");
    jsonStr = jsonStr.replace("\\", "\\\\");
    jsonStr = jsonStr.replace("'", "\\'");
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World</title>
<script>
    var data = JSON.parse('<%=jsonStr%>');
     alert(data.datas);
</script>
</head>
<body>
</body>
</html>

これにより、正しくJSON文字列がJSONオブジェクトに読み込まれた。余談であるが、JavaScript用に"\"をエスケープした場合、Unicodeエスケープシーケンス変換した文字(例えば、"\u0027")に使われている"\"もエスケープされ"\\u0027"に変換されておかしくならないかという思うかもしれない。
実は、"\\u0027"はJavaScriptによって"\u0027"と解釈され、それがJSON.parseの引数に与えれるため、JSON側で、Unicodeエスケープシーケンスとして解釈されているのだ。つまり前回のXSS対策の場合と同じ形のものがJSONに読み込まれており、むしろこちらの方が意図した動作となっているのである。JavaScript用のエスケープをする前は、実はHTML側でUnicodeエスケープシーケンスとして解釈されていたのだ。うーん、奥が深い。

おわりに

結局JavaScriptエスケープだったという地味なオチ。この連載を始めたときに、この記事を書きたいと思っていたので、とりあえず完結としたい。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaとJacksonでJSON その③HTMLにJSONを埋め込んでJavaScript から利用する

はじめに

JSONを使う場合、JavaScriptからAjax経由でデータの送受信を行うケースが多いと思う。しかしながら、サーバからHTMLを受信したタイミングで、JSONデータを受け取ってJavaScriptで利用したいケースもある。この場合、サーバから返却するHTMLの中にJSONデータを埋め込んで、それをJavaScriptのオブジェクトとして読み込むことになる。PHPを利用した場合は、HTML に JSON データを埋め込んで JavaScript から利用するに記載の事例があったが、我らがJava(Servlet/JSP)による事例がなかったため、悪戦苦闘した結果をここに残しておく。

環境

  • Java 1.8
  • Tomcat 8.0.53
  • Jackson 2.10.1

まずは何も考えずにやってみよう⇒失敗

サーバ側

サーバ側は以下の通りとした。"<"や">"については、前回同様、HTMLのタグとして解釈される恐れがあることから、Unicodeエスケープシーケンス変換はそのままとしている。前回までとの違いは、JSON文字列を、HttpRequestのパラメータとして保存し、それをJSPに処理させている点だ。詳しくはクライアント側の方で解説する。
また、意地悪データとして、"Programmer"⇒"Programmer\"のようにデータの最後に\を入れてみた。

ServletTest2.java
package servletTest;

import java.io.IOException;
import java.util.List;
import java.util.ArrayList;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletContext;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

@WebServlet("/helloworld2")
public class ServletTest2 extends HttpServlet {
    private static final long serialVersionUID = 1L;
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        //Javaオブジェクトに値をセット
        JsonBean jsonBean = new JsonBean();
        jsonBean.setId(1);
        jsonBean.setName("kimisyo");
        List<String> datas = new ArrayList<>();
        datas.add("Programmer\\");
        datas.add("Data Scientist<script>alert('hello!')</script>");
        jsonBean.setDatas(datas);

        ObjectMapper mapper = new ObjectMapper();
        mapper.getFactory().setCharacterEscapes(new CustomCharacterEscapes());

        //JavaオブジェクトからJSONに変換
        String testJson = mapper.writeValueAsString(jsonBean);

        //JSON文字列をrequestにセット
        request.setAttribute("jsonStr", testJson);
        ServletContext sc = getServletContext();
        sc.getRequestDispatcher("/clientTest2.jsp").forward(request, response);

    }
}

クライアント側

クライアント側はjspに処理を記載している。HttpRequestのパラメータとして設定されたJSON文字列を一旦Javaの変数に保存し、それをJavaScriptの中のJSONのParseの引数に設定している。
これがうまくいけば、dataというオブジェクトを通して、JSONのハンドリングが可能となる。

clientTest2.jsp
<%@ page contentType="text/html; charset=UTF-8"%>
<%
    String jsonStr = (String)request.getAttribute("jsonStr");
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World</title>
<script>
    var data = JSON.parse('<%=jsonStr%>');
     alert(data.datas);
</script>
</head>
<body>
</body>
</html>

失敗状況

上のServletを実行してみると、JSON.parseのところでSCRIPT1014: 文字が正しくありません。というJavaScriptのエラーがでる(IEのコンソールで確認)。最終的にブラウザに出力されたHTMLは以下の通りだ。
JSONに入れた\は、Jacksonによって\\にちゃんとエスケープされているし、"<"や">"はUnicodeエスケープシーケンスに変換されている。一見問題なさそうに見える。

さぁ、何がまずかったのか考えて見よう。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World</title>
<script>
    var data = JSON.parse('{"id":1,"name":"kimisyo","datas":["Programmer\\","Data Scientist\u003Cscript\u003Ealert(\u0027hello!\u0027)\u003C\u002Fscript\u003E"]}');
    alert(data.datas);
</script>
</head>
<body>
    <div id="test"></div>
</body>
</html>

失敗原因

失敗原因はJavaScript文字列のエスケープ漏れだ。JavaScriptでは、\はエスケープ用の文字として使われる。このため、"Programmer\\"はJavaScriptの文字列として"Programmer\"と認識される。これがJSON.Parseに引き渡されるが、JSONでも本来"\"の文字自体は、"\\"のようにエスケープしなければならないため、不正なJSONデータとして扱われるのだ。

対策

対策としては、JavaScript用のエスケープ処理をかました上でJSON.parseの引数に与えればよい。修正ソースを以下に記載しておく。ここではJavaScript文字列のエスケープとして、"\"と"'"をエスケープするための処理を入れている。

clientTest2.jsp
<%@ page contentType="text/html; charset=UTF-8"%>
<%
    String jsonStr = (String)request.getAttribute("jsonStr");
    jsonStr = jsonStr.replace("\\", "\\\\");
    jsonStr = jsonStr.replace("'", "\\'");
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World</title>
<script>
    var data = JSON.parse('<%=jsonStr%>');
     alert(data.datas);
</script>
</head>
<body>
</body>
</html>

これにより、正しくJSON文字列がJSONオブジェクトに読み込まれた。
余談であるが、JavaScript用に"\"をエスケープした場合、Unicodeエスケープシーケンス変換した文字(例えば、"\u0027")に使われている"\"もエスケープされ"\\u0027"に変換されておかしくならないかという思うかもしれない。
実は、"\\u0027"はJavaScriptによって"\u0027"と解釈され、それがJSON.parseの引数に与えれるため、JSON側で、Unicodeエスケープシーケンスとして解釈されているのだ。つまり前回のXSS対策の場合と同じ形のものがJSONに読み込まれており、むしろこちらの方が意図した動作となっているのである。JavaScript用のエスケープをする前は、実はHTML側でUnicodeエスケープシーケンスとして解釈されていたのだ。うーん、奥が深い。

おわりに

結局JavaScriptエスケープだったという地味なオチ。この連載を始めたときに、この記事を書きたいと思っていたので、とりあえず完結としたい。

ちなみに本記事のサーバ側の例で、WEBフレームワークを使わずにServletを使って説明している理由は、別にServletしか使えない、Servletを使いたいわけではなく、本記事のテーマに関係ない要素は除外し、本質的な部分のみにフォーカスしたかったためである。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

51歳からのプログラミング 備忘 android ダイアログに複数のViewを表示

ダイアログに複数のViewを表示するには、Viewを配置したLinearLayoutを、ダイアログにインフレートする。後で使いたい機能なのでちょっと備忘メモ。

構成
1.MainActivity.java
2.MyDialog.java
3.dilog_my.xml

MainActivity
protected void onCreate(final Bundle savedInstanceState){
  ...
  AppCompatDialogFragment dialog = new MyDialog();
  dialog.show(getSupportFragmentManager(),null);
}
MyDialog.java
public class MyDialog extends AppCompatDialogFragment{
   @Override
   public Dialog onCreateDialog(Bundle savedInstanceState){
      AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
      LayoutInflater inflater     = requireActivity().getLayoutInflater();
      //                            getActivity().getLayoutInflater();でもいけた
      builder.setView(inflater.inflate(R.layout.dialog_my,null))
             .setPositiveButton("OK",new DialogInterface.OnClickListener(){
                 @Override 
                 public void onClick(DialogInterface dialogInterface,int i){
                        // OKボタンを押した時の処理
                 }
             }).setNegativeButton("cancel",new DialogInterface.OnClickListener(){
                 @Override 
                 public void onClick(DialogInterface dialogInterface,int i){
                        // CANCELボタンを押した時の処理
                 }
             });
             return builder.create();
      }
}
dialog_my.xml
// インフレートさせるレイアウト
// EditTextを2つ配置してみる

<?xml version="1.0" encodeing="utf-8" ?>
<LinearLayout xmls:android="http://schemas.android.com/apk/res/android"
   android:orientation="vertical"
   android:layout_width="match_parent"
   android:layout_height="match_parent">

   <EditText
       android:id="@+id/edit1"
       android:inputType="numberPassword"
       android:layout_width="match_parent"
       android:layout_height="match_parent"/>

   <EditText
       android:id="@+id/edit2"
       android:layout_width="match_parent"
       android:layout_height="match_parent"/>

</LinearLayout>

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaFXでルートノードから下位ノードに向かって辿る

現在、JavaFXを使用してアプリを作成しているのですが、動的にノード(コンポーネント)を削除や追加する必要が出てきました。
しかし、ルートノードから下位ノードに向かってノードを辿っていく情報がほとんど無く、試行錯誤した方法を記載します。(ひとまず、調べたコンポーネントのみ記します)

環境
OS : macOS X Catalina
統合開発環境 : Eclipse 2019-12
Java : AdoptOpenJDK 11.0.5
JavaFX : 11.0.2
SceneBuilder : 11.0

NodeTree
1: AnchorPane ←ルートノード 
2: ┣━━━ MenuBar
3: ┣━┳━ AnchorPane ②
4:  ┗━┳━ SplitPane Horizontal(左右分割) ③
5:    ┣━━━ AnchorPane
6:    ┗━━━ PieChart ④
7: ┗━━━ AnchorPane

① ⇢ ② ⇢ ③ ⇢ ④ の順にノードを辿っていきます。

sample.java
 1: public Scene setPieChart(Stage stage) {
 2:     Scene scene = stage.getScene();
 3:     AnchorPane ap2 = (AnchorPane)this.scene.getRoot(); 
 4:     AnchorPane ap3 = (AnchorPane)ap2.getChildren().get(1); 
 5:     SplitPane spHorizontal = (SplitPane)ap3.getChildren().get(0); 
 6:     ObservableList<?> ol = (ObservableList<?>) spHorizontal.getItems(); 
 7:     PieChart pieChart = (PieChart)ol.get(1); 
 8:
 9:     return scene;
10: }

注意点として、SplitPaneの子ノードを取得しようとした場合、5行目のようにgetItems()ObservableList<?>型のオブジェクトを取得します。
この例で言うと、ObservableList<?>型のオブジェクトの中には0番目の要素にNodeTreeの②のAnchorPane、1番目の要素にPieChartがセットされています。なので、sample.javaの7行目でol.get(1)しているのはそのためです。

あともうひとつ、これは動作上の注意点なのですが、PieChartの親ノードをAnchorPaneなどのコンテナにすると、描画したグラフがウィンドウのリサイズに合わせて拡大縮小されなくなります。そのため、SplitPaneの直下にPieChartを配置しています。

他に引っかかりそうなところとしてはScrollPaneがあります。
具体的に、ScrollPaneの子ノードがAnchorPaneとするとAnchorPaneの取得方法は以下のようになります。

sample2.java
 1: AnchorPane anchorPane = (AnchorPane) scrollPane.contentProperty().get();

とりあえず調べたところはここまでです。
また、開発しているときに調べる必要が出てきた場合には追記していこうと思います。

自分の備忘録も兼ねてですが、皆さんのお役に立てば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaFXでルートノードから下位ノードに向かって辿る 〜道順は知ってるんだけど〜

現在、JavaFXを使用してアプリを作成しているのですが、動的にノード(コンポーネント)を削除や追加する必要が出てきました。
しかし、ルートノードから下位ノードに向かってノードを辿っていく情報がほとんど無く、試行錯誤した方法を記載します。(ひとまず、調べたコンポーネントのみ記します)

環境
OS : macOS X Catalina
統合開発環境 : Eclipse 2019-12
Java : AdoptOpenJDK 11.0.5
JavaFX : 11.0.2
SceneBuilder : 11.0

NodeTree
1: AnchorPane ←ルートノード ①
2: ┣━━━ MenuBar
3: ┣━┳━ AnchorPane ②
4: ┃ ┗━┳━ SplitPane ←Horizontal(左右分割) ③
5: ┃   ┣━━━ AnchorPane
6: ┃   ┗━━━ PieChart ④
7: ┗━━━ AnchorPane

① ⇢ ② ⇢ ③ ⇢ ④ の順にノードを辿っていきます。

sample.java
 1: public Scene setPieChart(Stage stage) {
 2:     Scene scene = stage.getScene();
 3:     AnchorPane ap2 = (AnchorPane)this.scene.getRoot(); 
 4:     AnchorPane ap3 = (AnchorPane)ap2.getChildren().get(1); 
 5:     SplitPane spHorizontal = (SplitPane)ap3.getChildren().get(0); 
 6:     ObservableList<?> ol = (ObservableList<?>) spHorizontal.getItems(); 
 7:     PieChart pieChart = (PieChart)ol.get(1); 
 8:
 9:     return scene;
10: }

注意点として、SplitPaneの子ノードを取得しようとした場合、5行目のようにgetItems()ObservableList<?>型のオブジェクトを取得します。
この例で言うと、ObservableList<?>型のオブジェクトの中には0番目の要素にNodeTreeの②のAnchorPane、1番目の要素にPieChartがセットされています。なので、sample.javaの7行目でol.get(1)しているのはそのためです。

あともうひとつ、これは動作上の注意点なのですが、PieChartの親ノードをAnchorPaneなどのコンテナにすると、描画したグラフがウィンドウのリサイズに合わせて拡大縮小されなくなります。そのため、SplitPaneの直下にPieChartを配置しています。

他に引っかかりそうなところとしてはScrollPaneがあります。
具体的に、ScrollPaneの子ノードがAnchorPaneとするとAnchorPaneの取得方法は以下のようになります。

sample2.java
 1: AnchorPane anchorPane = (AnchorPane) scrollPane.contentProperty().get();

とりあえず調べたところはここまでです。
また、開発しているときに調べる必要が出てきた場合には追記していこうと思います。

自分の備忘録も兼ねてですが、皆さんのお役に立てば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む