JavaでXMLデータを読み取ってMap / List に格納する

# はじめに
実装内容にてXMLデータを扱うプロジェクトの際にXML文字列から要素を抽出して、
ListクラスやMapクラスに格納する処理を調査していたのでそのアウトプットで記事にします。

# 環境
* Windows 11
* Java 17
* Juniper

# XMLデータから要素を取得して、データ構造に格納する
まずは、Mapクラスに格納する方法から、続いてListクラスに格納する方法と紹介していきます。
Listクラスに格納する際にちょっとした注意点とその対策方法も記載していますので、
併せて読んでいただけたら幸いです。

## XMLデータ例
前提として扱うデータ形式は下記のようになっています。
`<data>`タグから下に何階層かになっており、`<item>` タグの中に操作対象となる
 `<item-id>` タグと `<name>` タグがあります。

それぞれの `<item-id>` と `<name>` の中身を取得していきます。
```xml
<?xml version=\"1.0\" encoding=\"UTF-8\"?>
    <data>
        <element>
            <item>
                <item-id>1</item-id>
                <name>りんご</name>
            </item>
            <item>
                <item-id>2</item-id>
                <name>バナナ</name>
            </item>
            <item>
                <item-id>3</item-id>
                <name>スイカ</name>
            </item>
        </element>
    </data>
```


## Mapクラスに格納する場合
先にコードの方を紹介します。

#### App.java
```java:App.java
import net.juniper.netconf.NetconfException;

public class App {
    public static void main(String[] args) throws NetconfException {
        String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?> \r\n" + //
                "<data>\r\n" +
                "    <element>\r\n" + //
                "        <item>\r\n" + //
                "            <item-id>1</item-id>\r\n" + //
                "            <name>りんご</name>\r\n" + //
                "        </item>\r\n" + //
                "        <item>\r\n" + //
                "            <item-id>2</item-id>\r\n" + //
                "            <name>バナナ</name>\r\n" + //
                "        </item>\r\n" + //
                "        <item>\r\n" + //
                "            <item-id>3</item-id>\r\n" + //
                "            <name>スイカ</name>\r\n" + //
                "        </item>\r\n" + //
                "    </element>\r\n" + //
                "</data>\r\n" + //
                "";

        XMLConvertor xmlConvertor = new XMLConvertor();
        xmlConvertor.getItemsByXML(xml);

        for (Integer portNumber : xmlConvertor.getItemMap().keySet()) {
            System.out.println(portNumber + "：" + xmlConvertor.getItemMap().get(portNumber));
        }
    }
}
```
App.javaファイルにてXMLConvertorクラスの`getItemsByXML()`メソッドを呼び出すと同時に
引数に対象のXML文字列を渡してあげます。

#### XMLConvertor.java
```java:XMLConvertor.java
:
import net.juniper.netconf.NetconfException;

public class XMLConvertor {

  /**
   * XMLから抽出した要素を格納するMap
   */
  private Map<Integer, String> itemMap = new HashMap<>();

  /**
   * XML文字列から要素を取得
   * @param _resMessage
   * @throws NetconfException
   */
  public void getItemsByXML(String _resMessage) throws NetconfException {

    try {
      // ドキュメントオブジェクトを取得
      Document document = toDocument(_resMessage);
      Element element = document.getDocumentElement();

      // <item>タグを子孫要素をNodeListで取得
      NodeList elemList = element.getElementsByTagName("item");

      // 子孫要素分回す
      for (int i = 0; i < elemList.getLength(); i++) {
        Node elemChild = elemList.item(i);
        NodeList elemChildList = elemChild.getChildNodes();
        Integer itemId = null;
        String itemName = null;
        for (int j = 0; j < elemChildList.getLength(); j++) {
          Node elemChildNode = elemChildList.item(j);
          if (elemChildNode.getNodeName().equals("item-id")) {
            itemId = Integer.parseInt(elemChildNode.getTextContent());
          } else if (elemChildNode.getNodeName().equals("name")) {
            itemName = elemChildNode.getTextContent();
          }

          if (itemId != null && itemName != null) {
            itemMap.put(itemId, itemName);
          }
        }
      }
    } catch (Exception e) {
      System.out.println(e.getMessage());
    }
  }

  /**
   * XML文字列からドキュメントオブジェクトに変換
   * 
   * @param _xml
   * @return Document ドキュメントオブジェクト
   * @throws ParserConfigurationException
   * @throws SAXException
   * @throws IOException
   */
  public Document toDocument(String _xml) throws ParserConfigurationException, SAXException, IOException {
    try (StringReader reader = new StringReader(_xml)) {
      InputSource source = new InputSource(reader);
      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
      DocumentBuilder builder = factory.newDocumentBuilder();
      return builder.parse(source);
    }
  }

}

```

続いて、XMLConvertor.javaの中身も紹介します。
主に `getItemsByXML()` メソッドの中で変換処理を行っています。
`getItemsByXML()` メソッドの中の `toDocument()` メソッドでは、ドキュメントオブジェクトを取得しています。
その後ElementクラスやNodeList、Nodeクラスを使用してXMLデータを解析していきます。


XMLデータの解析を進めていく中でNodeNameが 
**item-id** と **name** のNodeに遭遇すると中身の要素を調べて、
一時的に``Integer itemId``と``String itemName``の変数に格納します。

それぞれの変数に要素が代入されたら、Mapに格納していく流れになっています。

:::note

この時、重複チェックをしていないのはMapクラスのput()メソッドでは
同じKeyとValueのペアがあると後から格納する要素で上書きされるためです。

:::



## Listクラスに格納する場合
次はListクラスに格納する方法です。
```java:XMLConvertorList.java
public class XMLConvertorList {

  /**
   * Itemリスト
   */
  private List<Item> itemList = new ArrayList<>();

  : // ゲッター

  /**
   * XML文字列から要素を取得
   * 
   * @param _resMessage
   * @throws NetconfException
   */
  public void getItemsByXML(String _resMessage) throws NetconfException {

    try {
      :

      // 子孫要素分回す
      for (int i = 0; i < elemList.getLength(); i++) {
        :
        Integer itemId = null;
        String itemName = null;
        for (int j = 0; j < elemChildList.getLength(); j++) {
          Node elemChildNode = elemChildList.item(j);
          if (elemChildNode.getNodeName().equals("item-id")) {
            itemId = Integer.parseInt(elemChildNode.getTextContent());
          } else if (elemChildNode.getNodeName().equals("name")) {
            itemName = elemChildNode.getTextContent();
          }

          if (itemId != null && itemName != null) {
            Item item = new Item(itemId, itemName);

            // 重複していたら何もしない
            if (!(itemList.contains(item))) {
              itemList.add(item);
            }
          }
        }
      }
    } catch (Exception e) {
      System.out.println(e.getMessage());
    }
  }
  : //toDocument() メソッド
}

```
Mapクラスに格納する方法とあまり変わりはありませんが、

ListクラスにはMapクラスのように同じ要素を上書きするようなメソッドはありませんので
ここでは重複チェックを行っています。

Listクラスに格納する際は別でデータクラスを用意して格納すると便利です。
その際に同じ内容のデータクラスを格納しないようにしないといけません。

そこで.`contains()` メソッドで重複チェックをして、重複していなかったら
格納するという処理の流れで行きます。
### `.contains()` メソッドで重複チェック後の実行結果

```
1：りんご
1：りんご
2：バナナ
2：バナナ
3：スイカ
3：スイカ
```
けれど実行して試してみると期待する結果になりません。
同じ要素が格納されてしまっており、`.contains()` メソッドが期待する挙動になってくれません。
### `.contains()` で重複チェックする際の注意点
`.contains()` メソッドではObject.equalsメソッドによって判定が行われるため、
オブジェクトクラスのequalsメソッドをオーバーライドしていないクラスでは`.contains()` メソッドの挙動が意図したものと異なる結果となってしまいます。
### 対処法
```java:Item.java
public class Item {

  /**
   * ID
   */
  private int itemId;

  /**
   * Name
   */
  private String itemName;

  /**
   * コンストラクター
   * 
   * @param _itemId
   * @param _itemName
   */
  public Item(int _itemId, String _itemName) {
    this.itemId = _itemId;
    this.itemName = _itemName;
  }

    : // ゲッター、セッター

  @Override
  public boolean equals(Object _obj) {
    if (_obj == null) {
      return false;
    }
    if (_obj instanceof Item) {
      Item item = (Item) _obj;
      return item.itemId == this.itemId && item.itemName.equals(this.itemName);
    }
    return false;
  }
}

```
Object.equals() メソッドをオーバーライドして、Itemクラスの重複をチェックできるようにします。

修正したコードで再度実行すると今度はちゃんと期待する結果になっており、
`.contains()` メソッドもちゃんと動いているようです。
```
1：りんご
2：バナナ
3：スイカ
```

## 登場するメソッド
ここで、処理の中で登場するメソッドをいくつか紹介します。

### NodeList `getElementsByTagName` (String name)
所定のタグ名とともに、すべての子孫ElementsのNodeListを文書順に返します。
引数に＊を指定するとすべてのタグに一致します。
### int `getLength` ()
リスト内のノード数です。有効な子ノード・インデックスの範囲は0以上length-1以下です。
### NodeList `getChildNodes` ()
このノードの子をすべて含むNodeList。
子が存在しない場合は、ノードが含まれていないNodeListになります。
# おわりに
調査していく中でXML文字列を操作するメソッドの挙動などを確認するのに苦戦してました。
また、重複チェックの事も当初頭になかったので改めて調査する中で勉強できて良かったです。
他にいい方法が絶対あると思うのでもし良かったらコメントで教えてくれたら幸いです。

## 参考

https://docs.oracle.com/javase/jp/8/docs/api/org/w3c/dom/Element.html

https://docs.oracle.com/javase/jp/8/docs/api/org/w3c/dom/Node.html

https://docs.oracle.com/javase/jp/8/docs/api/org/w3c/dom/NodeList.html

https://qiita.com/niwasawa/items/0dd4c1e778550e8212c8