20190417のJavaに関する記事は12件です。

Javaの別ファイルのクラスをnewした時のシンボルが見つけられませんエラー

/java/hello/Main.java
/java/hello/Hello.java

こういうディレクトリ構造でMain.javaからHello.javaにあるクラスをnewしようとした場合どうやってもコンパイルが通らずわからなかったが親フォルダのjavaフォルダからjavacするとコンパイルが通った。理屈がわからない-verboseで調べても[ソース・ファイルの検索パス: .]と出てくるので余計に混乱した。このディレクトリ構造の場合は以下でできました。ソースコードが間違ってなかったのでこの程度の事に気がつくのに半日かかりました。

コメント欄を見ていただいてもいいですがclassファイルがないことが原因だったようです。
その為、Hello.javaを先にjavacすると実行できます。

一応これでも通ったのでこれも載せときます。
javac hello/Main.java

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

【Java】配列ノート

配列

  • 配列の種類
    • 配列
    • 多次元配列
  • 配列のfor構文
    • 拡張for構文
  • int配列要素の平均計算のサンプルプログラム

配列の種類

配列

略記法が楽だから使っていきたいが、ガベージのことを考えると、newを使う正式な書き方もしっかり覚えておきたい。ガベージコレクションがあるとはいえ、省略されているところにnewがあると、しっかり自覚を持って略記法を書くこと。

// 正式な書き方
int[] scores; //配列変数の宣言
scores = new int[3]; //配列要素の宣言
scores[0]=10; //配列要素代入
scores[1]=20;
scores[2]=30;

// 略記法
int[] scores = new int[]{10,20,30};
int[] scores = {10,20,30};

// すべて等価

多次元配列

// 正式な書き方
String[][] scores = new String[2][3];
scores[0][0]="A1";
scores[0][1]="A2";
scores[0][2]="A3";
scores[1][0]="B1";
scores[1][1]="B2";
scores[1][2]="B3";

// 多次元配列の略記法
String[][] scores = {{"A1","A2","A3"},{"B1","B2","B3"}};

// すべて等価

配列のfor構文と拡張for構文

配列名.length:これで配列要素の個数を取得して利用

// 配列の作成
int[] scores = {10,20,30};

// 一般的なfor構文
for (int i=0; i<scores.length; i++){
    System.out.println(scores[i]);
}

// 拡張for構文
for (int value:scores){
    System.out.println(values);
}

多次元配列の場合

// 配列の作成
int scores[][]={{10,11,12},{21,22,23}};

// 一般的なfor構文
for(int i=0; i<scores.length; i++){
    for(int ii=0; ii<scores[i].length; ii++){
        System.out.println(scores[i][ii]);
    }
}

参考書籍

スッキリわかるJava入門第2版
Pp.138-166

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

Java上でカラム情報を付与してPostgresqlのcopyコマンドを実行する方法

はじめに

題材としてピンポイント過ぎて弱い気がしますが執筆。

PostgreSQLのcopyコマンドは、
ファイルをテーブルに格納する際に高速で処理を行う事ができます。
しかし、CSVをそのまま登録することはできるが、
値を補完したいって場合に、

  1. CSVファイルを読み込む。
  2. カラムを付与してCSVファイルを作成する。
  3. CopyManagerでcopyコマンドを実行してCSVをロードする。

って、順番で処理をしていると、
ストレージへの書込が2度(ファイルとDB)走るので、
一本化した方が良いと思い実装。

Javaでcopyコマンドをナイーブに実行するには

簡単に書くとこんな感じ、

  public static long copy(Connection conn, String filePath, String tableName) throws Exception {
    CopyManager copyManager = new CopyManager((BaseConnection) conn);
    Reader reader = new BufferedReader(new InputStreamReader(new FileInputStream(filePath), "UTF8"));
    String sql = "copy " + tableName + " FROM STDIN WITH DELIMITER ','";
    long result = copyManager.copyIn(sql, reader);
    reader.close();
    return result;    
  }

CopyManager にコネクションを渡して、
CSVのReaderクラスインターフェースのインスタンスを作成して、
copyInに渡して流し込む。
この時、CSVがそのままテーブルのカラム構成になっている事が前提。

どこで付与するか?

Readerを自前で作成して、
Readするタイミングでシステムカラムを付与してあげようと思い、
以下のようなReadメソッドを実装したReaderクラスを作成。

public class CsvFileWithSysColReader extends Reader {
  /** カラムを付与した文字列を格納するキュー */
  private final Queue<Character> csvBuffer;
  /** CSVのReader コンストラクタで引数で設定してあげる */
  private final BufferedReader reader = new ArrayDeque<>();
//~~略~~
  /**
   * コンストラクタ
   * 
   * @param reader ラップするBufferdReader
   * @throws IOException
   */
  public CsvFileWithSysColReader(final BufferedReader reader)
      throws IOException {
    // 引数にskipHeaderとかのBooleanを持たせてヘッダスキップ機能を持たせるのも良いかも。
    this.reader = reader;
  }

  @Override
  public int read(final char[] cbuf, final int off, final int len) throws IOException {
    int readCount = len;
    // CSVの読込バッファサイズがCopyコマンドの読込桁数未満だったらファイルから読み取る。
    while (this.csvBuffer.size() < len) {
      // ファイルの最終だった場合は、終了して現在の桁数のみ返す。
      if (loadLine() == 0) {
        readCount = this.csvBuffer.size();
        break;
      }
    }
    // 読込があった場合はその桁数を返す。
    if (readCount != 0) {
      for (int i = off; i < readCount; i++) {
        cbuf[i] = this.csvBuffer.poll();
      }
      return readCount;
    } else {
      // 無かった場合は終端の-1を返却する。
      return -1;
    }
  }

  /**
   * 行ロード
   * 
   * @return ロード結果文字数
   * @throws IOException
   */
  private int loadLine() throws IOException {
    final String line = this.reader.readLine();
    if (line == null) {
      // 取得行が無ければ0を返却して終了する。
      return 0;
    } else {
      // addSysCol:システムカラムを付与する関数です、好きに編集してください。
      // この辺りでチェックとかしてエラーカラムを付与するとかも可能
      final String lineWithSysCol= addSysCol(line);
      for (int i = 0; i < lineWithSysCol.length(); i++) {
        this.csvBuffer.add(lineWithSysCol.charAt(i));
      }
      return lineWithSysCol.length();
    }
  }
//~~略~~
}

そしてこんな感じで実行する。

  public static long copy(final Connection conn, final String filePath, final String tableName) throws Exception {
    final CopyManager copyManager = new CopyManager((BaseConnection) conn);
    final Reader reader = new CsvFileWithSysColReader(new BufferedReader(new InputStreamReader(new FileInputStream(filePath), "UTF8")));
    final String sql = "copy " + tableName + " FROM STDIN WITH DELIMITER ','";
    final long result = copyManager.copyIn(sql, reader);
    reader.close();
    return result;    
  }

上記CsvFileWithSysColReader.addSysCol(String line)の実装で以下のように幅が広がります。

  1. CSVに無い情報を外部から渡して付与する。
  2. 妥当性検証して、結果をロードするテーブルカラムに設定する
  3. テーブルカラム桁数に合わせて桁あふれをカットする
  4. 連番を振る
  5. etc...

おわりに

アプリケーション開発において、
I/Oが固定される事が多いので、
Interfaceでラップクラスを作ってそこで処理を吸収すれば
どうにかなる事が多いです。

ちな、
実際は色々な機能を付けて実装したのですが、
そこから機能を落として要点だけを書くようにしたのですが、
抜けがあったらごめんなさい。。。

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

【Java】制御構文ノート

制御構文

構造化定理

  • 順次
  • 分岐
    • if, switch
  • 繰り返し
    • while,for

分岐

  • if
  • switch
// if構文
int r=new java.util.Random().nextInt(2);
if (r<1){
    System.out.println("This is true by 'if'.");
}else{
    System.out.println("This is false by 'if'.");
}

// switch構文
int s=new java.util.Random().nextInt(3);
switch(s){
    case 0:
        System.out.println("This is Case.0 by 'switch'.");
        break;
    case 1:
        System.out.println("This is Case.1 by 'switch'.");
        break;
    case 2:
        System.out.println("This is Case.2 by 'switch'.");
        break;
    default:
        System.out.println("This is default by 'switch'.");
}

繰り返し

  • while
  • do while
  • for
// while構文
int w=1;
while(w<4){
    System.out.println("Count No."+w+" by 'while'.");
    w++;
}

// do while構文
int dw=3;
do{
    System.out.println("Count No."+dw+" by 'do while'.");
    dw--;
    System.out.println("Count decrease -1 by 'do while'.");
}while(dw>0);


// for構文
for(int i=1; i<=3; i++){
    System.out.println("Count No."+i+" by 'for'.");
}

「繰り返しの中断」という概念

  • continue:ループを続ける。
  • break:ループを抜ける。
// 繰り返しの中断
for(int j=5; i<=1; i--){
    if(j==3){
        continue;
    }
    if(j==2){
        break;
    }
    System.out.println("Count No."+i+" by 'for/continue(3)/break(2)'.");
}

参考書籍

スッキリわかるJava入門第2版
Pp.098-133.

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

SpringBootでのローカルファイルダウンロード覚書

ファイルダウンロード実装手順

はじめに

業務のためのメモ代わりの殴り書き。あとできちんとした記事にする。

ファイルダウンロード

最終的にはControllerから Resouce型のファイルをreturnすればいい
ローカルのファイルをDLさせるのであれば

AnyController.java
Resource resource = new FileSystemResource("ここにファイルパス");
return resource;

ファイルを任意の名前でダウンロードさせたい

Controllerの引数に HttpServeletResponse response を追加。
responseのフィールド変数(header)に適切な値をセットする。
return 等はそのままでいい。Spring側がよしなに対応してくれる。

AnyController.java
// 引数の記述は省略
Resource resource = new FileSystemResource("ここにファイルパス");
response.setHeader("Content-Disposition", "attachment; filename=" + "ここにファイル名");
return resource;

全角文字をファイル名に使いたい

ファイル名をURLエンコードされたものにすればOK

AnyController.java
// 引数の記述は省略
Resource resource = new FileSystemResource("ここにファイルパス");
response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode("ここにファイル名", "UTF-8"));
return resource;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javaで新元号試してみた

Java SE 8 が新元号に対応したらしい

2019年4月17日公開の Java SE 8 Update 211 にて検証。

Sample.java
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("GGGGy年M月d日")
        .withChronology(JapaneseChronology.INSTANCE)
        .withLocale(Locale.JAPAN);

JapaneseDate jd = JapaneseDate.of(2019, 4, 29);

for (int i = 0; i < 5; i++) {
    System.out.println(dtf.format(jd.plus(i, ChronoUnit.DAYS)));
}
実行結果
平成31年4月29日
平成31年4月30日
令和1年5月1日
令和1年5月2日
令和1年5月3日

令和になりました。やりましたね。

ところで、もうすぐゴールデンウィークですね

元号ばかり気にしていて、新元号の最初の年を「元年」と呼ぶのをすっかり失念していましたよ。トラブルにならないといいですね。

楽しいゴールデンウィークをお過ごしください。

一応「元年」も対応してみた

Sample2.java
DateTimeFormatter dtf = new DateTimeFormatterBuilder()
        .appendText(ChronoField.ERA, TextStyle.FULL)
        .appendText(ChronoField.YEAR_OF_ERA, Collections.singletonMap(1L, "元"))
        .appendLiteral("年")
        .appendValue(ChronoField.MONTH_OF_YEAR)
        .appendLiteral("月")
        .appendValue(ChronoField.DAY_OF_MONTH)
        .appendLiteral("日")
        .toFormatter()
        .withChronology(JapaneseChronology.INSTANCE)
        .withLocale(Locale.JAPAN);

もういっそ西暦にしませんか?(結論)

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

jenvでjavaのバージョン管理

jenvでjavaのバージョン管理

はじめに

現在(2019/4/17)のjavaの最新バージョンは12です。

環境

  • MacOS Mojave 10.14.4
  • HomeBrew 2.1.1

jenvのインストール

$ brew install jenv

.bash_profileに以下を追記し、パスを通す。

# jenvのPATHを通す
export JENV_ROOT="$HOME/.jenv"
if [ -d "${JENV_ROOT}" ]; then
  export PATH="$JENV_ROOT/bin:$PATH"
  eval "$(jenv init -)"
fi

JDKのインストール

brew caskでインストールする方法もありますが、現環境ではbrewからインストールできなかったのでOracleからインストールします。

ここからインストール

私は今回は12,10,8をインストールしたので、説明はその前提で記述します。

$ /usr/libexec/java_home -V

上のコマンドを実行すると、インストールされているJDKのバージョンが一覧で表記されます。

私の場合コマンドを実行して返ってきたのが以下になります。

12, x86_64: "Java SE 12"    /Library/Java/JavaVirtualMachines/jdk-12.jdk/Contents/Home
10.0.2, x86_64: "Java SE 10.0.2"    /Library/Java/JavaVirtualMachines/jdk10.0.2.jdk/Contents/Home
1.8.0_202, x86_64:  "Java SE 8"/Library/Java/JavaVirtualMachines/jdk1.8.0_202.jdk/Contents/Home

/Library…jdk-12.jdk/Contents/Homeのように表示される部分が以降で必要になります。・

$ jenv add /Library...jdk-12.jdl/Contents/Home

のように、先ほどの部分をjenv add以降に入力し、jenvに登録します。

$ jenv versions

のコマンドでjenvに登録されているjavaのバージョンが一覧で表示されます。

GlobalのJavaのバージョンを変更する

$ jenv global 12
$ jenv rehash # 必要ない場合もある

$jenv gobal [version]の後に

$jenv rehashをしなくちゃ自分の場合はバージョンが切り替わりませんでした。

LocalのJavaのバージョンを変更する

$ jenv local 12

$jenv local [version]で変更します

変更できているかは

java -version

で、確認して、変更が反映されていたらおkです!!

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

Gsonがめんどくさく感じる人のためのコード

Json をこういう風↓に作りたいときもありますよね。。(org.jsonみたいに)
需要は無いのでしょうか?

JavaBeanみたいのをチマチマ定義するのも面倒ですし。
キー名固定みたいのもダサいですよね。setUser(String)とか。

package hello.gson;

public class HelloGson {

    public static void main(String[] args) {

        MyJsonObj obj = new MyJsonObj();
        obj.put("key1-1", "value1");
        obj.put("key1-2", "value2");
        {
            MyJsonObj obj2 = new MyJsonObj();
            obj2.put("key2-1", "value2");
            {
                String[] ss = { "aa", "bb", "cc" };
                obj2.put("arr2-1", ss);
            }
            obj.put("key1-3", obj2);
        }
        {
            String[] ss = { "aa", "bb", "cc" };
            obj.put("arr1-1", ss);
        }

        // {"key1-1":"value1","key1-2":"value2","key1-3":{"key2-1":"value2","arr2-1":["aa","bb","cc"]},"arr1-1":["aa","bb","cc"]}
        System.err.println(obj.toJson());

    }

}


というわけで、Gsonにデフォルトで存在するのかもしれませんが、作ってみました。
(すでに存在していればどなたかご指摘ください。)

package hello.gson;

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;

public class MyJsonObj {
    JsonObject root = new JsonObject();
    private JsonObject getRoot() {return root;}
    public void put(String k, Boolean v) {root.addProperty(k, v);}
    public void put(String k, Character v) {root.addProperty(k, v);}
    public void put(String k, Number v) {root.addProperty(k, v);}
    public void put(String k, String v) {root.addProperty(k, v);}
    public void put(String k, MyJsonObj o) {root.add(k, o.getRoot());}

    public void put(String k, Object[] oo) {
        JsonArray innerArray = new JsonArray();
        for (Object o : oo) {
            if (o instanceof Boolean) {
                innerArray.add((Boolean) o);
            } else if (o instanceof Character) {
                innerArray.add((Character) o);
            } else if (o instanceof Number) {
                innerArray.add((Number) o);
            } else if (o instanceof String) {
                innerArray.add((String) o);
            }
        }
        root.add(k, innerArray);
    }

    public String toJson() {
        return new Gson().toJson(root);
    }
    public String toJsonPrettyPrinting() {
        Gson g = new GsonBuilder().setPrettyPrinting().create();
        return g.toJson(root);
    }
}


めでたしめでたし。

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

Gsonがめんどくさく感じる人のためのコード: GsonでJSONを組み立てる

Json をこういう風↓に作りたいときもありますよね。。(org.jsonみたいに)
需要は無いのでしょうか?

JavaBeanみたいのをチマチマ定義するのも面倒ですし。
キー名固定みたいのもダサいですよね。setUser(String)とか。

package hello.gson;

public class HelloGson {

    public static void main(String[] args) {

        MyJsonObj obj = new MyJsonObj();
        obj.put("key1-1", "value1");
        obj.put("key1-2", "value2");
        {
            MyJsonObj obj2 = new MyJsonObj();
            obj2.put("key2-1", "value2");
            {
                String[] ss = { "aa", "bb", "cc" };
                obj2.put("arr2-1", ss);
            }
            obj.put("key1-3", obj2);
        }
        {
            String[] ss = { "aa", "bb", "cc" };
            obj.put("arr1-1", ss);
        }

        // {"key1-1":"value1","key1-2":"value2","key1-3":{"key2-1":"value2","arr2-1":["aa","bb","cc"]},"arr1-1":["aa","bb","cc"]}
        System.err.println(obj.toJson());

    }

}


というわけで、Gsonにデフォルトで存在するのかもしれませんが、作ってみました。
(すでに存在していればどなたかご指摘ください。)

package hello.gson;

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;

public class MyJsonObj {
    JsonObject root = new JsonObject();
    private JsonObject getRoot() {return root;}
    public void put(String k, Boolean v) {root.addProperty(k, v);}
    public void put(String k, Character v) {root.addProperty(k, v);}
    public void put(String k, Number v) {root.addProperty(k, v);}
    public void put(String k, String v) {root.addProperty(k, v);}
    public void put(String k, MyJsonObj o) {root.add(k, o.getRoot());}
    public void put(String k, Object[] oo) {
        JsonArray innerArray = new JsonArray();
        for (Object o : oo) {
            if (o instanceof Boolean) {
                innerArray.add((Boolean) o);
            } else if (o instanceof Character) {
                innerArray.add((Character) o);
            } else if (o instanceof Number) {
                innerArray.add((Number) o);
            } else if (o instanceof String) {
                innerArray.add((String) o);
            }
        }
        root.add(k, innerArray);
    }
    public String toJson() {
        return new Gson().toJson(root);
    }
    public String toJsonPrettyPrinting() {
        Gson g = new GsonBuilder().setPrettyPrinting().create();
        return g.toJson(root);
    }
}

めでたしめでたし。

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

この時代にJackson formatter XMLでJavaBeanとXMLを変換してみた

経緯

お察しください

準備

pomに下記を追加

<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
    <version>2.9.8</version>
</dependency>

詳細な使い方

http://fasterxml.github.io/jackson-dataformat-xml/javadoc/2.9/

もろもろのクラス準備

前提

・Beanのデータを作るときにLombokの@Builderを使いました(本記事では触れてません)
  https://projectlombok.org/

Bean

TestBean.java
@Data
@Builder
@JacksonXmlRootElement(localName = "top_element")
public class TestBean implements Serializable {

    // isAttribute = trueを付与すると属性になる
    @JacksonXmlProperty(localName = "id", isAttribute = true)
    private String topElement;

    @JacksonXmlProperty(localName = "test_field")
    private String testField;

    // ネストしたBeanも書ける
    @JacksonXmlProperty(localName = "test_inner_bean")
    private TestInnerBean testInnerBean;

    // Beanのリストをフィールドに持てる
    @JacksonXmlElementWrapper(localName = "dtls")
    @JacksonXmlProperty(localName = "dtl")
    private List<DtlsInnerDto> dtlsInnerDto;

ネストするBean

TestInnerBean.java
@SuppressWarnings("serial")
@Data
@Builder
public class TestInnerBean implements Serializable {

    @JacksonXmlProperty(localName = "inner_field_one")
    private String innerFieldOne;

    @JacksonXmlProperty(localName = "inner_field_two")
    private String innerFieldTwo;
}

ネストするBeanのリスト

DtlsInnerDto.java
@SuppressWarnings("serial")
@Data
@Builder
public class DtlsInnerDto implements Serializeble {
    @JacksonXmlProperty(localName = "dtl_item")
    private String dtlItem;
}

Bean→XML変換処理のコード

import com.fasterxml.jackson.dataformat.xml.XmlMapper;

String xml = new XmlMapper().writeValueAsString(Bean組み立て処理());

変換結果

<top_element id="xxx">
  <test_field>xxx</test_field>
  <test_inner_bean>
     <inner_field_one>xxx</inner_field_one>
     <inner_field_two>xxx</inner_field_two>
  </test_inner_bean>
 <dtls>
    <dtl>xxx</dtl>
    <dtl>yyy</dtl>
    <dtl>zzz</dtl>
  </dtls>
</top_element>

XML→Bean変換処理のコード

import com.fasterxml.jackson.dataformat.xml.XmlMapper;

Beanクラス bean = new XmlMapper().readValue(xml文字列, Beanクラス.class)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dockerコンテナ上で起動したアプリケーションで時間がおかしい

はじめに

どうも@chan_kakuです
今回は社内ツールをDocker化した際に問題が発生したので、その問題と今回打った対策、そして苦労した点など紹介していきます。

問題

今回はJavaのアプリケーションで問題が発生しました。
その問題というのは、とある箇所で、DBにて永続化させる時間をnew Date()で現在時間をとって永続化させるような処理でした。
今までは、Dockerを使っておらず、ホストサーバのタイムゾーンに依存してましたので、JSTで時間が取れており、何も問題がありませんでした。
しかしながら、このツールをDocker化した後にこのnew Date()での現在時間がずれるというような問題が発生してしまいました。

原因と推測

そもそも自分はDockerについての知識があまりなく、Java側のコードに問題があると推測していました。
しかしながら、上記にあるように、現在時刻をnew Date()でとっていたため、その推測は外れてしまいました。
次の推測としては、ホストサーバのタイムゾーンでした。しかしながら、dateコマンドを打つと、JSTと表示されたため、こちらも外れました。
僕の考えられる推測が尽きてしまったため、Dockerに詳しい方々に聞いたところ、どうやらDocker内にも時間があるらしく、こちらの時間がずれているのではないかという情報を得ました。
そこで、以下のコマンドでDocker内に入り、同じようにDockerコンテナ内のタイムゾーンを調べました

docker run -t -i (対象のdocker image)  /bin/bash → docker起動して中に入る
# date

ここでは先ほどと同じようにJSTとなっておりました。

対策

もはや、ここで万策尽きたと嘆いていたところ、とある方から鶴の一声が、、、
「Docker内で/etc/timezone の中身見てみて〜」
この方を信じ、Docker内に入り、/etc/timezone をみてみたところ、ここがUTC となっていました
もしやということでDockerfileで /etc/timezoneAsia/Tokyoになるように以下のように修正しました

ENV TZ="Asia/Tokyo"
RUN echo $TZ > /etc/timezone

// timezone関連以外の記載は省略

今回あえてechoで変えた理由としては、ツールの要件的に時間系のタイムゾーンはJSTであることが求められていたので、ここはDockerfileに記載すべきだと判断しました

終わりに

Dockerについての知識があまりなかったためにいろんなところで苦労はしましたが、ひとまず解決して良かったです!
もっとこうした方がスマートだよなどのマサカリをお待ちしております

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

XXEとJava

XXEとJava

XXEとは

XXE(XML External Entity)とは、XMLの外部参照機能を利用して、サーバ内部のファイル内容を取得(漏えい)したり、内部ネットワーク上のファイルにアクセスしたりする不正行為

XXEは、XMLを扱うアプリケーションで発現する可能性があるので、XML文書を取り扱う際には注意する必要がある。

JavaとXML

JavaでXMLを扱うには、javax.xml.parsers.DocumentBuilderを利用が考えられる。

よって、以降は、javax.xml.parsers.DocumentBuilderを使って、再現させたり、対策を考えてみたりしよう。

結論

XXE基本編のリンクには、JavaVMのプロパティで制御する方法もあるので、手っ取り早くはそちらかもしれない。
その一方で、Javaプログラマであれば、リゾルバを自作して制御することも難しくはないはず。

JavaでXXEを発現させる

下記のソースコードのプログラムは、第一引数にXMLファイル、第二引数に外部参照の動作モードを指定するようになっているので、既定の動作モードで、XMLファイル(C:\z\xmlxxe\in1.xml)を読んでみる。

こんな感じ。

xxe-java-defo.png

Java の場合は、既定ではXXE攻撃を受けてしまうということになる。

外部参照のオブジェクトをNULLにする

次はjavax.xml.parsers.DocumentBuilder#setEntityResolver()メソッドで「NULL」を与えてみる。

こんな感じ。

xxe-java-null.png

.NET Framework とは異なり、Nullは既定のリゾルバオブジェクトということになるので、Java の場合は、NULLでもXXE攻撃を受けてしまうということになる。

外部参照のオブジェクトを自作する

次は「外部参照機能を限定的に使いたいけど、XXEを防ぎたい」。ていうか「リゾルバの修正で外部参照機能を防ぎたい」という場合は、リゾルバを自作すればいい。

つまり、javax.xml.parsers.DocumentBuilder#setEntityResolver()メソッドの第一引数は「org.xml.sax.EntityResolver」インターフェイス型なので、それを継承したクラスを自作して、それをjavax.xml.parsers.DocumentBuilder#setEntityResolver()メソッドの第一引数に割り当てれば、外部参照を自由に制御する事ができる。

上書き必須のメソッドは、「org.xml.sax.InputSource.InputSource resolveEntity(String publicId,String systemId)」メソッドだけ。

これの肝になる部分は、第二引数に外部参照のURLを示すString型が来るので、それに応じた「org.xml.sax.InputSource.InputSource」型を返せばいいだけ。

そして、この「org.xml.sax.InputSource.InputSource」型からの適切な「java.io.InputStream」クラス、または「java.io.Reader」クラスを返すようにしてあげればよい。

例えば、認証が必要だったり、特定のURIだけ許可したり、というような個別カスタマイズが可能。

ソースコードの「exEntityResolver.java」の「exEntityResolverクラス」は、全てのURIを示すsystemIdに対して、最終的に空のjava.io.ByteArrayInputStreamを返すだけの「exInputSource」クラスを返すことにした。

こんな感じになる。

xxe-java-cust.png

ソースコード(XXEtest.java)

XXEtest.java
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;
import java.io.File;
import org.xml.sax.EntityResolver;

public class XXEtest{
 public static void main(String args[]){
  System.out.println("java.exe test <<inFile>> <<Resolver>>");
  if(0 < args.length){
   Boolean IsResolve = false;
   exEntityResolver myExEntityResolver = null;
   if(1 < args.length){
    if(args[1].equals("null") == true){
     IsResolve = true;
    }else{
     myExEntityResolver = new exEntityResolver();
    }
   }
   try{
    DocumentBuilderFactory tempDocumentBuilderFactory = DocumentBuilderFactory.newInstance();
    DocumentBuilder tempDocumentBuilder = tempDocumentBuilderFactory.newDocumentBuilder();
    if(IsResolve == true){
     tempDocumentBuilder.setEntityResolver(null);
    }else if(myExEntityResolver != null){
     tempDocumentBuilder.setEntityResolver(myExEntityResolver);
    }
    Document tempDocument = tempDocumentBuilder.parse(new File(args[0]));
    System.out.println("getTextContent  : " + tempDocument.getTextContent());
    System.out.print("getXmlStandalone: ");
    tempDocument.setXmlStandalone(true);
    System.out.println(tempDocument.getXmlStandalone());
    System.out.println("========Detail=================");
    PrintNode(tempDocument.getChildNodes(), "");
   }catch(Exception e){
    e.printStackTrace();
   }
  }
 }
 static void PrintNode(NodeList nodeList, String spacer){
  Node node = null;
  for(int i=0; i< nodeList.getLength(); i++){
   node = nodeList.item(i);
   String typeStr = "unknown";
   switch(node.getNodeType()){
    case Node.ELEMENT_NODE:
     typeStr = "ELEMENT_NODE";
     break;
    case Node.ATTRIBUTE_NODE:
     typeStr = "ATTRIBUTE_NODE";
     break;
    case Node.TEXT_NODE:
     typeStr = "TEXT_NODE";
     break;
    case Node.CDATA_SECTION_NODE:
     typeStr = "CDATA_SECTION_NODE";
     break;
    case Node.ENTITY_REFERENCE_NODE:
     typeStr = "ENTITY_REFERENCE_NODE";
     break;
    case Node.ENTITY_NODE:
     typeStr = "ENTITY_NODE";
     break;
    case Node.PROCESSING_INSTRUCTION_NODE:
     typeStr = "PROCESSING_INSTRUCTION_NODE";
     break;
    case Node.COMMENT_NODE:
     typeStr = "COMMENT_NODE";
     break;
    case Node.DOCUMENT_NODE:
     typeStr = "DOCUMENT_NODE";
     break;
    case Node.DOCUMENT_TYPE_NODE:
     typeStr = "DOCUMENT_TYPE_NODE";
     break;
    case Node.NOTATION_NODE:
     typeStr = "NOTATION_NODE";
     break;
   }
   System.out.println(spacer + "name =" + node.getNodeName() + ", type=" + typeStr);
   System.out.println(spacer + "value=" + node.getNodeValue());
   System.out.println(spacer + "getTextContent=" + node.getTextContent());
   if(node.hasChildNodes() == true){
    PrintNode(node.getChildNodes(), spacer + " ");
   }
  }
 }
}

ソースコード(exEntityResolver.java)

空の ByteArrayInputStream を返すだけの InputSource クラスを返すだけのリゾルバ

exEntityResolver.java
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import java.io.IOException;

public class exEntityResolver implements EntityResolver{
 public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException{
  System.out.println("publicId: " + publicId);
  System.out.println("systemId: " + systemId);
  return (InputSource)new exInputSource();
 }
}

ソースコード(exInputSource.java)

空の ByteArrayInputStream を返すだけの InputSource クラス。

exInputSource.java
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ByteArrayInputStream;
import java.io.Reader;
import java.io.InputStreamReader;

public class exInputSource extends InputSource{
 ByteArrayInputStream myStream;
 public exInputSource(){
  byte[] hako = new byte[0];
  this.myStream = new ByteArrayInputStream(hako);
 }
 public InputStream getByteStream(){
  return (InputStream)this.myStream;
 }
 public Reader getCharacterStream(){
  return (Reader)new InputStreamReader(this.myStream);
 }
}

戻る

XXE基本編へ戻る

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