Skip to content

Commit df50a5b

Browse files
committed
EDIFACT
1 parent 30b42ce commit df50a5b

4 files changed

Lines changed: 388 additions & 1 deletion

File tree

.textlintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
},
7373
"no-exclamation-question-mark": false,
7474
"ja-no-weak-phrase": false,
75-
"ja-no-continuous-kanji": {
75+
"max-kanji-continuous-len": {
7676
"max": 10
7777
}
7878
},
Lines changed: 387 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,387 @@
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 Elec­tronic 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の複雑なネスト構造を直感的なコードで表現できるようになりました。少々ニッチな領域ではありますが、モダンな開発環境でレガシーな仕様と向き合う際の参考になれば幸いです。
169 KB
Loading
16.4 KB
Loading

0 commit comments

Comments
 (0)