JavaFXで動的にテーブル列を設定する

# この記事について

将来的に書く予定の「JavaFX で DynamoDB Viewer作ってみた」記事の１ステップ。
結構大きな話になると思うので、少しずつ技術ポイント毎に記事を書いて、ある一定程度の要件を満たせた段階で前述まとめ記事書く予定。

前回記事：[DynamoDBの情報を読み込んでJavaFXで表示してみる](https://qiita.com/silverbox/items/eba824a9f003df67181e)

今回は、前回時点での課題になっていた、JavaFXのTableViewのカラム情報の動的設定。
テーブル名を指定してDynamoDBから情報を読み込んで、その情報に従って列を設定する。
各データ型に応じて変更したい部分もあるが、今回はすべて文字列扱いにする。

※前回の記事が基本になってます。メソッドなど再説明していない部分があります。
　今回終了時のソースはこちら。[Github](https://github.com/silverbox/dynamodbtool/blob/2nd-dynamictable/src/main/java/com/silverboxsoft/classes/DynamoDbResult.java) 

# 今回の進捗

テーブル部分に変更はないけど、カラムも動的に作ってます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530376/117cd443-f240-31c3-1a0b-b676f094775e.png)

# 画面レイアウト変更

- 既存列を削除
- テーブル名、パーティションキー名、指定値入力用に TextField３つ追加
- これらのTextFieldに fx:id を指定

# java側修正

## TableViewのカラムを動的にする。

- TableViewのクラス指定を```ObservableList<String>```にする。
- TableViewのカラムを読み込む度にクリア＆再作成
- カラム再作成時には、setCellValueFactoryを使用してデータマッピングを指定する。

```java:DynamoDbToolController.java
package com.silverboxsoft.controller;

import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;

import com.silverboxsoft.classes.DynamoDbCondition;
import com.silverboxsoft.classes.DynamoDbConditionJoinType;
import com.silverboxsoft.classes.DynamoDbConditionType;
import com.silverboxsoft.classes.DynamoDbResult;
import com.silverboxsoft.dao.DynamicDao;

import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;

public class DynamoDbToolController implements Initializable {

	// テーブルの表示データクラスをObservableList<String>にする
	@FXML
	TableView<ObservableList<String>> tableResultList;

	@FXML
	TextField txtFldTableName;

	@FXML
	TextField txtFldColumnName;

	@FXML
	TextField txtFldCondValue;

	@Override
	public void initialize(URL location, ResourceBundle resources) {
	}

	@FXML
	protected void actLoad(ActionEvent ev) {
		String tableName = txtFldTableName.getText();
		DynamicDao dao = new DynamicDao();
		List<DynamoDbCondition> conditionList = new ArrayList<>();
		DynamoDbCondition cond = new DynamoDbCondition();
		cond.setColumnName(txtFldColumnName.getText());
		cond.setConditionType(DynamoDbConditionType.EQUAL);
		cond.setValue(txtFldCondValue.getText());
		conditionList.add(cond);

		// DynamoDBの結果を取得してテーブルにセットする
		DynamoDbResult result = dao.getResult(tableName, DynamoDbConditionJoinType.AND, conditionList);
		setTable(result);
	}

	private void setTable(DynamoDbResult result) {
		tableResultList.getItems().clear();

		tableResultList.getColumns().clear();
		for (int colIdx = 0; colIdx < result.getColumnCount(); colIdx++) {
			// 後述するDynamoDbResult からデータ名取得して設定
			String columnName = result.getDynamoDbColumn(colIdx).getColumnName();
			TableColumn<ObservableList<String>, String> dataCol = new TableColumn<>(columnName);
			final int finalColIdx = colIdx;
			//データマッピングを指定する
			dataCol.setCellValueFactory(param -> new ReadOnlyObjectWrapper<>(param.getValue().get(finalColIdx)));
			tableResultList.getColumns().add(dataCol);
		}

		tableResultList.getItems().addAll(result.getResultItems());
	}
}
```

## DynamoDBの結果を取得するクラス作成

後々の為に、検索条件を指定できるようにしておく。

```java:DynamicDao.java
package com.silverboxsoft.dao;

import java.util.HashMap;
import java.util.List;

import com.silverboxsoft.classes.DynamoDbCondition;
import com.silverboxsoft.classes.DynamoDbConditionJoinType;
import com.silverboxsoft.classes.DynamoDbResult;

import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.QueryRequest;
import software.amazon.awssdk.services.dynamodb.model.QueryResponse;

public class DynamicDao {

	private DynamoDbClient ddb;

	public DynamicDao() {
		Region region = Region.AP_NORTHEAST_1;
		this.ddb = DynamoDbClient.builder().region(region).build();
	}

	public DynamoDbResult getResult(String tableName, DynamoDbConditionJoinType conditionJoinType,
			List<DynamoDbCondition> conditionList) {
		HashMap<String, String> attrNameAlias = new HashMap<String, String>();
		HashMap<String, AttributeValue> attrValues = new HashMap<String, AttributeValue>();
		StringBuilder conditionExpression = new StringBuilder();

		for (DynamoDbCondition dbCond : conditionList) {
			attrNameAlias.put(dbCond.getAlias(), dbCond.getColumnName());
			attrValues.put(":" + dbCond.getColumnName(), AttributeValue.builder().s(dbCond.getValue()).build());

			if (conditionExpression.length() > 0) {
				conditionExpression.append(conditionJoinType.getJoinStr());
			}
			conditionExpression.append(dbCond.getConditionExpression());
		}

		System.out.println(conditionExpression.toString());
		QueryRequest queryReq = QueryRequest.builder()
				.tableName(tableName)
				.keyConditionExpression(conditionExpression.toString())
				.expressionAttributeNames(attrNameAlias)
				.expressionAttributeValues(attrValues)
				.build();

		QueryResponse response = ddb.query(queryReq);
		return new DynamoDbResult(response);
	}
}

```

## DynamoDBの結果を保持するクラス作成

QueryResponse（JavaSDKにおける、DynamoDBの結果クラス）からTableViewに表示する形式に変更する。
[AttributeVal](https://sdk.amazonaws.com/java/api/2.0.0/software/amazon/awssdk/services/dynamodb/model/AttributeValue.html) が参考になる。

```java:DynamoDbResult.java
package com.silverboxsoft.classes;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.QueryResponse;

public class DynamoDbResult {

	private List<DynamoDbColumn> columnList = new ArrayList<>();
	private Map<String, Integer> colNameIndex = new HashMap<>();
	private List<ObservableList<String>> resItems = new ArrayList<>();

	public DynamoDbResult(QueryResponse response) {
		for (Map<String, AttributeValue> resItem : response.items()) {
			analyzeOneItem(resItem);

			ObservableList<String> record = FXCollections.observableArrayList();
			for (int colIdx = 0; colIdx < columnList.size(); colIdx++) {
				DynamoDbColumn dbCol = getDynamoDbColumn(colIdx);
				String columnName = dbCol.getColumnName();
				record.add(getAttrString(dbCol, resItem.get(columnName)));
			}
			this.resItems.add(record);
		}
	}

	public int getColumnCount() {
		return columnList.size();
	}

	public int getRecordCount() {
		return resItems.size();
	}

	public List<ObservableList<String>> getResultItems() {
		return resItems;
	}

	public DynamoDbColumn getDynamoDbColumn(int index) {
		return columnList.get(index);
	}

	// DynamoDBの結果からカラム名とデータ形式抽出して保存
	private void analyzeOneItem(Map<String, AttributeValue> resItem) {
		for (String colName : resItem.keySet()) {
			AttributeValue attrVal = resItem.get(colName);
			if (!colNameIndex.containsKey(colName)) {
				DynamoDbColumn dbCol = new DynamoDbColumn();
				dbCol.setColumnName(colName);
				dbCol.setColumnType(getDynamoDbColumnType(attrVal));
				colNameIndex.put(colName, columnList.size());
				columnList.add(dbCol);
			}
		}
	}

	// https://sdk.amazonaws.com/java/api/2.0.0/software/amazon/awssdk/services/dynamodb/model/AttributeValue.html
	private DynamoDbColumnType getDynamoDbColumnType(AttributeValue attrVal) {
		if (attrVal.hasSs()) {
			return DynamoDbColumnType.STRING_SET;
		} else if (attrVal.hasNs()) {
			return DynamoDbColumnType.NUMBER_SET;
		} else if (attrVal.hasBs()) {
			return DynamoDbColumnType.BINARY_SET;
		} else if (attrVal.hasM()) {
			return DynamoDbColumnType.MAP;
		} else if (attrVal.hasL()) {
			return DynamoDbColumnType.LIST;
		}
		// StringとNumberを判断するメソッドが無い様なので、AttributeValのString出力から判断
		String attrValStr = attrVal.toString();
		if (attrValStr.startsWith("AttributeValue(N=")) {
			return DynamoDbColumnType.NUMBER;
		}
		System.out.println(attrValStr);
		return DynamoDbColumnType.STRING;
	}

	// 今はString型とNumberだけ対応
	private String getAttrString(DynamoDbColumn dbCol, AttributeValue attrVal) {
		if (dbCol.getColumnType() == DynamoDbColumnType.STRING) {
			return attrVal.s();
		} else if (dbCol.getColumnType() == DynamoDbColumnType.NUMBER) {
			return attrVal.n();
		}
		return attrVal.toString();
	}
}
```

DynamoDbColumnType というenumクラスも作成。

```java:DynamoDbColumnType.java
package com.silverboxsoft.classes;

public enum DynamoDbColumnType {
	STRING, NUMBER, BOOLEAN, STRING_SET, NUMBER_SET, MAP, LIST, BINARY, BINARY_SET, NULL;
}
```

# あとがき

今回はソースコードばかりになってしまった。基本技術は、後述の参考ページで書かれている感じ。
あと、今回提示したソース以外にも細かいソースは色々あるので、興味のある方は [Github](https://github.com/silverbox/dynamodbtool/blob/2nd-dynamictable/src/main/java/com/silverboxsoft/classes/DynamoDbResult.java) で。
次回は開発時などに使うlocalDynamoDbに対応とか、テーブルリスト表示とかしてみる予定。

# 参考にさせて頂いたページ

## 公式ページ
[AttributeVal](https://sdk.amazonaws.com/java/api/2.0.0/software/amazon/awssdk/services/dynamodb/model/AttributeValue.html)

## 皆さんの良記事
[java fxmlのTableViewに動的な列と行を追加する方法](https://www.366service.com/jp/qa/ea8970c54bdd7a454a50390b9049cbca)