|
| 1 | +--- |
| 2 | +title: "EDIFACTメッセージ処理をJavaでスクラッチ実装した試行錯誤" |
| 3 | +date: 2026/01/14 00:00:00 |
| 4 | +postid: a |
| 5 | +tag: |
| 6 | + - Java |
| 7 | +category: |
| 8 | + - IoT |
| 9 | +thumbnail: /images/2026/20260114a/thumbnail.jpg |
| 10 | +author: 辻大志郎 |
| 11 | +lede: "Web系のシステム開発やデータ連携においてデータフォーマットにJSONを利用することは一般的ですが、製造系の基幹システムとのデータ連携においてEDIFACTを利用するケースがあります。EDIFACTはあまり馴染みのない、あるいは聞きなれないデータフォーマットかと思いますが..." |
| 12 | +--- |
| 13 | +# はじめに |
| 14 | + |
| 15 | +製造・エネルギーグループの辻です。Web系のシステム開発やデータ連携においてデータフォーマットにJSONを利用することは一般的ですが、製造系の基幹システムとのデータ連携においてEDIFACTを利用するケースがあります。EDIFACTはあまり馴染みのない、あるいは聞きなれないデータフォーマットかと思いますが、製造業あるいは他業種においても歴史あるデータフォーマットです。またEDIFACTデータを処理するにあたってはエンタープライズ向けのETL製品やEDIミドルウェアが導入されているケースがよくあると思います。ただ、今回、EDIFACTを扱うJavaアプリケーションをスクラッチ開発するニーズがありました。 |
| 16 | + |
| 17 | +そこで本記事では、まずEDIFACTとは何か?ということを簡単に触れたのち、JavaでEDIFACTデータを処理するためのライブラリ選定や実装アプローチの概要を紹介します。 |
| 18 | + |
| 19 | +# そもそもEDIFACTとは? |
| 20 | + |
| 21 | +EDIFACTとは、国連が定めた電子データ交換の国際標準規格です。正式名称はUN/EDIFACT(the United Nations rules for Electronic Data Interchange for Administration, Commerce and Transport)[^3]と呼ばれます。テキストベースのデータ構造ですが、JSONのようにキー名を持たず、位置と順序でデータの意味が決まります。`'` 、 `+` 、 `:` などの文字を用いて文字列を区切り、メッセージ仕様に基づいて階層構造やデータの詳細がわかるデータフォーマットです。 |
| 22 | + |
| 23 | +[^3]: https://unece.org/trade/uncefact/introducing-unedifact |
| 24 | + |
| 25 | +EDIFACTのサンプルデータを以下に記載しました。なお、実際の伝送データは改行されていませんが、可視性向上のために改行しています。 |
| 26 | + |
| 27 | +```text EDIFACTサンプルデータ |
| 28 | +UNB+UNOA:2+SENDER_ID+RECEIVER_ID+251228:1500+1234' |
| 29 | +UNH+ME001+ORDERS:D:96A:UN' |
| 30 | +BGM+220+PO12345+9' |
| 31 | +DTM+137:20251228:102' |
| 32 | +NAD+BY+BUYER_CODE::92' |
| 33 | +NAD+SE+SELLER_CODE::92' |
| 34 | +LIN+1+1+PRODUCT_A:EN' |
| 35 | +QTY+21:100:PCE' |
| 36 | +PRI+AAA:500::NTP' |
| 37 | +LIN+2+1+PRODUCT_B:EN' |
| 38 | +QTY+21:50:PCE' |
| 39 | +PRI+AAA:1200::NTP' |
| 40 | +UNS+S' |
| 41 | +CNT+2:2' |
| 42 | +UNT+14+ME001' |
| 43 | +UNZ+1+1234' |
| 44 | +``` |
| 45 | + |
| 46 | +`UNB` や `UNH`、 `DTM` といった謎の単語(EDIFACTの用語でセグメントと呼びます)と付随する文字列が現れました。何を意味しているか一見、理解不能のように見えます。 |
| 47 | + |
| 48 | +ところが、JSONフォーマットに変換するとなんとなくイメージがわく方も多いのではないでしょうか?以下は先ほどのEDIFACTメッセージを擬似的にJSONに変換したデータです。 |
| 49 | + |
| 50 | +```json EDIFACTサンプルデータ(疑似変換) |
| 51 | +{ |
| 52 | + "interchangeHeader": { |
| 53 | + "senderId": "SENDER_ID", |
| 54 | + "receiverId": "RECEIVER_ID", |
| 55 | + "timestamp": "2025-12-28T15:00:00", |
| 56 | + "controlReference": "1234", |
| 57 | + "syntaxIdentifier": "UNOA:2" |
| 58 | + }, |
| 59 | + "messageBody": { |
| 60 | + "messageHeader": { |
| 61 | + "messageType": "ORDERS", |
| 62 | + "version": "D:96A:UN", |
| 63 | + "messageReferenceNumber": "ME001" |
| 64 | + }, |
| 65 | + "documentDetails": { |
| 66 | + "documentNameCode": "220", |
| 67 | + "documentNumber": "PO12345", |
| 68 | + "messageFunctionCode": "9", |
| 69 | + "documentDate": "2025-12-28" |
| 70 | + }, |
| 71 | + "parties": [ |
| 72 | + { |
| 73 | + "role": "BUYER", |
| 74 | + "id": "BUYER_CODE", |
| 75 | + "codeListQualifier": "92" |
| 76 | + }, |
| 77 | + { |
| 78 | + "role": "SELLER", |
| 79 | + "id": "SELLER_CODE", |
| 80 | + "codeListQualifier": "92" |
| 81 | + } |
| 82 | + ], |
| 83 | + "lineItems": [ |
| 84 | + { |
| 85 | + "lineNumber": 1, |
| 86 | + "productId": "PRODUCT_A", |
| 87 | + "quantity": { |
| 88 | + "value": 100, |
| 89 | + "unit": "PCE" |
| 90 | + }, |
| 91 | + "price": { |
| 92 | + "amount": 500, |
| 93 | + "type": "NetPrice" |
| 94 | + } |
| 95 | + }, |
| 96 | + { |
| 97 | + "lineNumber": 2, |
| 98 | + "productId": "PRODUCT_B", |
| 99 | + "quantity": { |
| 100 | + "value": 50, |
| 101 | + "unit": "PCE" |
| 102 | + }, |
| 103 | + "price": { |
| 104 | + "amount": 1200, |
| 105 | + "type": "NetPrice" |
| 106 | + } |
| 107 | + } |
| 108 | + ], |
| 109 | + "summary": { |
| 110 | + "totalLineCount": 2 |
| 111 | + } |
| 112 | + } |
| 113 | +} |
| 114 | +``` |
| 115 | + |
| 116 | +EDIFACTの文字列を、上記のように階層構造に変換できる理由は、メッセージタイプ(業務プロセスごとに定義されたデータ構造の仕様)により構造が定義されているためです。上記で紹介した例は、UN/EDIFACTの `ORDERS` (発注)というメッセージタイプに基づくサンプルメッセージです。 |
| 117 | + |
| 118 | +https://service.unece.org/trade/untdid/d96a/trmd/orders_t.htm |
| 119 | + |
| 120 | +その他にも `DESADV`(出荷通知)、`INVOIC`(請求)など、様々な業務プロセスに対応するメッセージタイプが定義されています。それぞれのメッセージタイプごとに、どのセグメントを、どの順序で、何回繰り返すかというデータ構造が定義されています。 |
| 121 | + |
| 122 | +ただ、実務上はUN/EDIFACTの国際標準をベースにしつつも、業界ごとに独自の規約(サブセット)を定義して運用することが一般的です。たとえば自動車業界では JAMA(日本自動車工業会) [^1] が[EDIFACT導入ガイドライン](https://www.jama.or.jp/operation/it/biz_sys/download.html)を制定しています。さらに、業界内でも個社ごとに仕様がカスタマイズされているケースは少なくありません。 |
| 123 | + |
| 124 | +[^1]: https://www.jama.or.jp/ |
| 125 | + |
| 126 | +# JavaのEDIFACTライブラリ選定 |
| 127 | + |
| 128 | +EDIFACTの概要を紹介しました。次は、EDIFACTメッセージをJavaアプリケーションで処理するにあたって、ベースとなるライブラリ選定プロセスを紹介します。Javaライブラリをざっくりと調査&簡易検証した結果、候補として以下の2つに絞られました(ただ調査&簡易検証範囲は限定的です。他にもこのライブラリが便利だよ、という知見をお持ちの方、こっそりと教えて下さい)。 |
| 129 | + |
| 130 | +|案|ライブラリ名|リポジトリ|特徴| |
| 131 | +|-|-|-|-| |
| 132 | +|案1|Smooks|https://github.com/smooks/smooks|・高機能なデータ変換フレームワーク<br>・EDIFACTメッセージの仕様を定義したXMLファイルを用意することで、メッセージのパースとBeanへデータマッピングが可能| |
| 133 | +|案2|Staedi|https://github.com/xlate/staedi|・ストリームベースの軽量EDIパーサ<br>・処理フローをアプリケーションコードで制御する必要あり<br>・Beanへのマッピング機能はない| |
| 134 | + |
| 135 | +結論としては、案2のStaediを採用しました。当初、案1のSmooksを主として検討していましたが、EDIFACTメッセージの定義ファイルを用意することが困難であると判明しました。UN/EDIFACTの標準規格の定義ファイル自体はSmooksの標準機能として提供されており、当初はよさそうに見えていました[^2]。 |
| 136 | + |
| 137 | +[^2]: https://github.com/smooks/smooks-edi-cartridge/tree/b60b715cfc84cfffdecb1cfbbf7065af08107ecd/edifact-schemas/src/main/resources |
| 138 | + |
| 139 | +しかし、実務上は個社別にカスタマイズされているEDIFACTの定義ファイルを用意する必要がありました。いくつか試行錯誤しましたが、この個社別にカスタマイズされている定義ファイルを作成することが困難であるとわかりました。これがノックアウトファクターとなり、代替案であるStaediを採用することにしました。 |
| 140 | + |
| 141 | +# 実装アプローチ |
| 142 | + |
| 143 | +Staediは、EDIFACTデータの読み書きに特化したストリームベースのパーサライブラリです。メッセージ構造の解釈や処理フローはアプリケーションコードに委ねられています。実装方針は大きく2通り考えられました。 |
| 144 | + |
| 145 | +1. [StaediのREADME](https://github.com/xlate/staedi?tab=readme-ov-file#reading-edi)にあるような、イベント駆動(ステートマシン)的なアプローチ |
| 146 | +2. 全データをメモリ上のツリー構造として扱う、構文解析的なアプローチ |
| 147 | + |
| 148 | +今回は2の方針を採用しました。実務で採用されている複雑なEDIFACT仕様にそって処理することを考えたとき、主に以下の理由から2のアプローチのほうがコードの保守性が高いと判断したためです。 |
| 149 | + |
| 150 | +* コードの構造からメッセージ仕様を直接的に読み取れる |
| 151 | +* 入力データの並び順とシステムが必要とする出力データの順序が異なる場合でも、ツリー構造であれば柔軟にデータを取り出せる |
| 152 | +* 構文木としてデバッグできるため、構造定義とデータ抽出定義のどちらに問題があるかを特定しやすい |
| 153 | + |
| 154 | +2のアプローチではEDIFACTのセグメントを一度JSONノードのリストとして展開し、そのリストをカーソルを使って走査しながら、階層構造を構築します。EDIFACTのメッセージは `'` や `+` や `:` などの文字により区切られていますが、このような字句解析はStaediにまかせます。字句解析結果のJSONノードをアプリケーションコードで構造解析し、ビジネスロジックで必要なデータを抽出します(なお、本記事ではデータの抽出部分は割愛します)。 |
| 155 | + |
| 156 | +*Java処理フロー概要* |
| 157 | +<img src="/images/2026/20260114a/Java処理フロー概要.jpg" alt="Java処理フロー概要.jpg" width="1200" height="371" loading="lazy"> |
| 158 | + |
| 159 | +## StaediとJacksonを用いたセグメントのリスト化 |
| 160 | + |
| 161 | +まず、Staediの `EDIStreamReader` とJacksonの `ObjectMapper` を組み合わせて、EDIFACTメッセージを `List<JsonNode>` に変換します。EDIFACTの各セグメントを扱いやすいJSONオブジェクトのリストにします。 |
| 162 | + |
| 163 | +```java |
| 164 | + private static final ObjectMapper MAPPER = new ObjectMapper(); |
| 165 | + |
| 166 | + /*** |
| 167 | + * EDIFACTデータを読み込み、セグメントのリストを取得する. |
| 168 | + * |
| 169 | + * @param edifactStr EDIFACT形式の文字列データ |
| 170 | + * @throws IOException 入出力例外 |
| 171 | + * @return セグメントのリスト |
| 172 | + */ |
| 173 | + public static List<JsonNode> readSegments(String edifactStr) throws IOException { |
| 174 | + var factory = EDIInputFactory.newFactory(); |
| 175 | + |
| 176 | + // 実データは標準仕様と細部の違いがあるため |
| 177 | + factory.setProperty(EDIInputFactory.EDI_VALIDATE_CONTROL_STRUCTURE, false); |
| 178 | + var stream = new ByteArrayInputStream(edifactStr.getBytes(StandardCharsets.UTF_8)); |
| 179 | + |
| 180 | + JsonNode rootNode; |
| 181 | + try (var ediReader = factory.createEDIStreamReader(stream); |
| 182 | + var jsonParser = factory.createJsonParser(ediReader, JsonParser.class)) { |
| 183 | + // StaediのReaderをJacksonのParserとして扱う |
| 184 | + rootNode = MAPPER.readValue(jsonParser, JsonNode.class); |
| 185 | + } |
| 186 | + |
| 187 | + // 各セグメントをJsonノードのリストとして取得 |
| 188 | + List<JsonNode> segmentList = new ArrayList<>(); |
| 189 | + for (JsonNode node : rootNode.path("data")) { |
| 190 | + segmentList.add(node); |
| 191 | + } |
| 192 | + |
| 193 | + return segmentList; |
| 194 | + } |
| 195 | +``` |
| 196 | + |
| 197 | +## カーソルの用意 |
| 198 | + |
| 199 | +次に現在どのセグメントを参照していているか?を管理するカーソルクラスを用意します。パース処理中に、次のセグメントが `UNH` メッセージであれば `UNH` メッセージ処理をする、といった先読みをできるようにしています。 |
| 200 | + |
| 201 | +```java |
| 202 | +/** |
| 203 | + * EDIFACTデータのカーソル操作クラス. |
| 204 | + */ |
| 205 | +public class EdifactCursor { |
| 206 | + private final List<JsonNode> nodes; |
| 207 | + private int position = 0; |
| 208 | + |
| 209 | + /** |
| 210 | + * コンストラクタ. |
| 211 | + * |
| 212 | + * @param nodes EDIFACTセグメントのリスト |
| 213 | + */ |
| 214 | + public EdifactCursor(List<JsonNode> nodes) { |
| 215 | + this.nodes = nodes; |
| 216 | + } |
| 217 | + |
| 218 | + /** |
| 219 | + * 現在位置のセグメント名を確認. |
| 220 | + * |
| 221 | + * @return セグメント名 |
| 222 | + */ |
| 223 | + public String peek() { |
| 224 | + if (position >= nodes.size()) return null; |
| 225 | + return nodes.get(position).path("name").asText(); |
| 226 | + } |
| 227 | + |
| 228 | + /** |
| 229 | + * 指定したタグであれば取得して位置を進める. |
| 230 | + * |
| 231 | + * @param expectedTag 期待するセグメント名 |
| 232 | + * @return JsonNode |
| 233 | + */ |
| 234 | + public JsonNode match(String expectedTag) { |
| 235 | + if (expectedTag.equals(peek())) { |
| 236 | + return nodes.get(position++); |
| 237 | + } |
| 238 | + return null; |
| 239 | + } |
| 240 | +} |
| 241 | +``` |
| 242 | + |
| 243 | +## パース処理 |
| 244 | + |
| 245 | +カーソルを用いて、メッセージをパースする処理を実装します。複数回登場しうるセグメント(`NAD`、`LIN` など)は先読みしながらループを回して処理します。コードの構造から、セグメントの流れ(`UNH` の次に `BGM` が登場するなど)や登場回数(1回なのか複数回)が、わかることが嬉しいポイントです。 |
| 246 | + |
| 247 | +```java |
| 248 | +/** |
| 249 | + * ツリー構造のノードとなるグループクラス |
| 250 | + */ |
| 251 | +public class EdifactGroup { |
| 252 | + private final String name; |
| 253 | + private final List<JsonNode> segments = new ArrayList<>(); |
| 254 | + private final List<EdifactGroup> children = new ArrayList<>(); |
| 255 | + |
| 256 | + /** |
| 257 | + * コンストラクタ. |
| 258 | + * |
| 259 | + * @param name グループ名 |
| 260 | + */ |
| 261 | + public EdifactGroup(String name) { |
| 262 | + this.name = name; |
| 263 | + } |
| 264 | + |
| 265 | + /** |
| 266 | + * ルートのパース処理. |
| 267 | + * |
| 268 | + * @param cursor EDIFACTカーソル |
| 269 | + */ |
| 270 | + public static EdifactGroup parseInterchange(EdifactCursor cursor) { |
| 271 | + EdifactGroup interchange = new EdifactGroup("Interchange") |
| 272 | + .addSegment(cursor.match("UNB")); |
| 273 | + while ("UNH".equals(cursor.peek())) { |
| 274 | + interchange.addChild(parseMessage(cursor)); |
| 275 | + } |
| 276 | + return interchange.addSegment(cursor.match("UNZ")); |
| 277 | + } |
| 278 | + |
| 279 | + /** |
| 280 | + * Message単位のパース. |
| 281 | + * |
| 282 | + * @param cursor EDIFACTカーソル |
| 283 | + */ |
| 284 | + private static EdifactGroup parseMessage(EdifactCursor cursor) { |
| 285 | + EdifactGroup message = new EdifactGroup("Message") |
| 286 | + .addSegment(cursor.match("UNH")) |
| 287 | + .addSegment(cursor.match("BGM")) |
| 288 | + .addSegment(cursor.match("DTM")); |
| 289 | + |
| 290 | + while ("NAD".equals(cursor.peek())) { |
| 291 | + message.addSegment(cursor.match("NAD")); |
| 292 | + } |
| 293 | + |
| 294 | + return message.addChild(parseDetailSection(cursor)) |
| 295 | + .addSegment(cursor.match("UNS")) |
| 296 | + .addSegment(cursor.match("CNT")) |
| 297 | + .addSegment(cursor.match("UNT")); |
| 298 | + } |
| 299 | + |
| 300 | + /** |
| 301 | + * 明細行単体のパース. |
| 302 | + * |
| 303 | + * @param cursor EDIFACTカーソル |
| 304 | + * @return 明細行グループ |
| 305 | + */ |
| 306 | + private static EdifactGroup parseLineItem(EdifactCursor cursor) { |
| 307 | + return new EdifactGroup("LineItem") |
| 308 | + .addSegment(cursor.match("LIN")) |
| 309 | + .addSegment(cursor.match("QTY")) |
| 310 | + .addSegment(cursor.match("PRI")); |
| 311 | + } |
| 312 | + |
| 313 | + /** |
| 314 | + * 明細セクションのパース. |
| 315 | + * |
| 316 | + * @param cursor EDIFACTカーソル |
| 317 | + * @return 明細セクショングループ |
| 318 | + */ |
| 319 | + private static EdifactGroup parseDetailSection(EdifactCursor cursor) { |
| 320 | + EdifactGroup detail = new EdifactGroup("Detail"); |
| 321 | + while ("LIN".equals(cursor.peek())) { |
| 322 | + detail.addChild(parseLineItem(cursor)); |
| 323 | + } |
| 324 | + return detail; |
| 325 | + } |
| 326 | + |
| 327 | + /** |
| 328 | + * セグメント追加. |
| 329 | + * |
| 330 | + * @param seg 追加するセグメント |
| 331 | + * @return 追加したセグメント |
| 332 | + */ |
| 333 | + public EdifactGroup addSegment(JsonNode seg) { |
| 334 | + if (seg != null) { |
| 335 | + this.segments.add(seg); |
| 336 | + } |
| 337 | + return this; |
| 338 | + } |
| 339 | + |
| 340 | + /** |
| 341 | + * 子グループ追加. |
| 342 | + * |
| 343 | + * @param group 追加する子グループ |
| 344 | + * @return 追加した子グループ |
| 345 | + */ |
| 346 | + public EdifactGroup addChild(EdifactGroup group) { |
| 347 | + if (group != null && !group.isEmpty()) { |
| 348 | + this.children.add(group); |
| 349 | + } |
| 350 | + return this; |
| 351 | + } |
| 352 | + |
| 353 | + private boolean isEmpty() { |
| 354 | + return segments.isEmpty() && children.isEmpty(); |
| 355 | + } |
| 356 | +} |
| 357 | +``` |
| 358 | + |
| 359 | +実行クラスや `toString()` などの実装は省略しますが、これらを組み合わせると、以下のようにEDIFACTメッセージを構造化して扱えます。 |
| 360 | + |
| 361 | +```txt 構造解析結果 |
| 362 | +[Interchange] |
| 363 | + UNB : UNOA:2 + SENDER_ID + RECEIVER_ID + 251228:1500 + 1234 |
| 364 | + [Message] |
| 365 | + UNH : ME001 + ORDERS:D:96A:UN |
| 366 | + BGM : 220 + PO12345 + 9 |
| 367 | + DTM : 137:20251228:102 |
| 368 | + NAD : BY + BUYER_CODE::92 |
| 369 | + NAD : SE + SELLER_CODE::92 |
| 370 | + UNS : S |
| 371 | + CNT : 2:2 |
| 372 | + [Detail] |
| 373 | + [LineItem] |
| 374 | + LIN : 1 + 1 + PRODUCT_A:EN |
| 375 | + QTY : 21:100:PCE |
| 376 | + PRI : AAA:500::NTP |
| 377 | + [LineItem] |
| 378 | + LIN : 2 + 1 + PRODUCT_B:EN |
| 379 | + QTY : 21:50:PCE |
| 380 | + PRI : AAA:1200::NTP |
| 381 | + UNT : 14 + ME001 |
| 382 | + UNZ : 1 + 1234 |
| 383 | +``` |
| 384 | + |
| 385 | +# まとめ |
| 386 | + |
| 387 | +JavaによるEDIFACT処理のスクラッチ開発について、ライブラリ選定から実装までの一連の流れを紹介しました。ストリームパーサであるStaediで字句解析、アプリケーション側で構文解析するアプローチを採用することで、EDIFACTの複雑なネスト構造を直感的なコードで表現できるようになりました。少々ニッチな領域ではありますが、モダンな開発環境でレガシーな仕様と向き合う際の参考になれば幸いです。 |
0 commit comments