はじめに
法務チームやフォレンジック分析者が、文書が改ざんされていないことを証明する必要がある場合、目に見えるコンテンツだけを確認しても不十分です。作者、作成日、リビジョン番号といった隠れたプロパティは、誰がいつファイルに手を加えたかを明らかにします。これらの微妙な変更を文書バージョン間で検出することは、各プロパティを手作業で確認しなければならないという、時間がかかりミスが起きやすい一般的な課題です。
GroupDocs.Metadata for Java は、すべてのメタデータフィールドをプログラム的に抽出し、2 つのバージョン間の構造化された差分を計算する方法を提供します。このチュートリアルでは、フルメタデータ差分、所有権変更検出、リビジョン履歴分析という 3 つの実用的アプローチを比較します。各手法は簡潔でコピー&ペースト可能なコードで示し、結果を CSV または JSON にエクスポートして監査レポートに利用する方法も併せて紹介します。
複数の関係者が数か月にわたって編集した契約書をレビューしたとき、見た目のテキストは同一でも所有者フィールドが静かに変わっていたことに気付きました。
2 つの文書バージョン間でどのメタデータフィールドが変更されたかを判別するには?
GroupDocs.Metadata は各ファイルを読み込み、取得可能なすべてのプロパティをマップに抽出した後、キーを走査して追加・削除・変更を分類します。ライブラリは組み込みタグとカスタムタグの両方に対応しているため、フォーマット固有のパーサを書かずに全体像を把握できます。結果は MetadataDiff オブジェクトとして取得でき、コンプライアンスレポート用にクエリしたりシリアライズしたりできます。
前提条件
- Java 8 以降
- GroupDocs.Metadata for Java 24.7(temporary license)
- 比較対象となる 2 つの文書ファイル(例:
contract_v1.pdfとcontract_v2.pdf)
インストール
Maven で依存関係を追加します。
<dependency>
<groupId>com.groupdocs</groupId>
<artifactId>groupdocs-metadata</artifactId>
<version>24.7</version>
</dependency>
方法 1 – フルメタデータ差分
この方法は すべて のメタデータプロパティを両バージョンから抽出し、追加・削除・変更エントリを報告します。
// CompareMetadataSets.run – returns a MetadataDiff object
Map<String, String> v1 = ExtractAllMetadata.run(pathV1);
Map<String, String> v2 = ExtractAllMetadata.run(pathV2);
MetadataDiff diff = new MetadataDiff();
// Detect added and changed properties
for (Map.Entry<String, String> e : v2.entrySet()) {
String key = e.getKey();
String val = e.getValue();
if (!v1.containsKey(key)) {
diff.added.put(key, val); // New property in v2
} else if (!v1.get(key).equals(val)) {
diff.changed.put(key, new String[]{v1.get(key), val}); // Value changed
}
}
// Detect removed properties
for (Map.Entry<String, String> e : v1.entrySet()) {
if (!v2.containsKey(e.getKey())) {
diff.removed.put(e.getKey(), e.getValue());
}
}
return diff;
重要ポイント:
- 包括的: カスタムタグを含むすべてのタグを取得します。
- シンプルなマップロジック: 外部の差分ライブラリは不要です。
- 結果オブジェクト:
added、removed、changedマップはそのまま後続処理に利用可能です。
💡 ヒント: 規制遵守のためにフル監査証跡が必要なときに使用してください。
方法 2 – 所有権変更の検出
法的紛争はしばしば、誰が文書を作成・編集したかに依存します。この方法は Creator、Editor、Manager、Company といった人物関連タグに焦点を当てます。
// DetectOwnershipChanges.run – returns a map of changed ownership fields
Map<String, String> v1 = readOwnership(pathV1);
Map<String, String> v2 = readOwnership(pathV2);
Set<String> allKeys = new HashSet<>(v1.keySet());
allKeys.addAll(v2.keySet());
Map<String, String[]> changes = new LinkedHashMap<>();
for (String key : allKeys) {
String oldVal = v1.getOrDefault(key, "<missing>");
String newVal = v2.getOrDefault(key, "<missing>");
if (!oldVal.equals(newVal)) {
changes.put(key, new String[]{oldVal, newVal});
}
}
return changes;
readOwnership は関連タグだけを取得します。
Map<String, String> result = new LinkedHashMap<>();
try (Metadata metadata = new Metadata(path)) {
if (metadata.getFileFormat() == FileFormat.Unknown) return result;
for (MetadataProperty p : metadata.findProperties(
new ContainsTagSpecification(Tags.getPerson().getCreator())
.or(new ContainsTagSpecification(Tags.getPerson().getEditor()))
.or(new ContainsTagSpecification(Tags.getPerson().getManager()))
.or(new ContainsTagSpecification(Tags.getCorporate().getCompany())))) {
String value = "";
if (p.getValue() != null && p.getValue().getRawValue() != null) {
value = String.valueOf(p.getValue().getRawValue());
}
result.put(p.getName(), value);
}
}
return result;
重要ポイント:
- ターゲット絞り: 身元情報に関わるプロパティのみを検査します。
- 明確な出力: 各エントリは
[old, new]形式で返されます。 - コンプライアンス対応: e‑discovery や契約所有権争いに最適です。
💡 ヒント: フル差分と組み合わせれば、広範囲かつ詳細な分析が可能です。
方法 3 – リビジョン履歴変更の検出
リビジョン番号、編集タイムスタンプ、印刷日付はエンドユーザーには見えませんが、フォレンジックタイムライン構築には不可欠です。この方法は時間関連タグに限定して抽出します。
Map<String, String> v1 = readRevision(pathV1);
Map<String, String> v2 = readRevision(pathV2);
Set<String> allKeys = new HashSet<>(v1.keySet());
allKeys.addAll(v2.keySet());
Map<String, String[]> changes = new LinkedHashMap<>();
for (String key : allKeys) {
String oldVal = v1.getOrDefault(key, "<missing>");
String newVal = v2.getOrDefault(key, "<missing>");
if (!oldVal.equals(newVal)) {
changes.put(key, new String[]{oldVal, newVal});
}
}
return changes;
readRevision は Modified、Created、Printed のタイムスタンプを抽出します。
Map<String, String> result = new LinkedHashMap<>();
try (Metadata metadata = new Metadata(path)) {
if (metadata.getFileFormat() == FileFormat.Unknown) return result;
for (MetadataProperty p : metadata.findProperties(
new ContainsTagSpecification(Tags.getTime().getModified())
.or(new ContainsTagSpecification(Tags.getTime().getCreated()))
.or(new ContainsTagSpecification(Tags.getTime().getPrinted())))) {
String value = "";
if (p.getValue() != null && p.getValue().getRawValue() != null) {
value = String.valueOf(p.getValue().getRawValue());
}
result.put(p.getName(), value);
}
}
return result;
重要ポイント:
- タイムライン再構築: ファイルが何回編集・印刷されたかを示します。
- 数値的差分: 急速なリビジョン増加の疑いを検出するのに有用です。
- 軽量: 3 つのタグだけをクエリするため、実行が高速です。
💡 ヒント: 特定の期限以降に文書が改ざんされていないことを証明したい場合に使用してください。
手法の比較 – いつどれを使うべきか
| 手法 | 最適な用途 | 主な利点 | 制限事項 |
|---|---|---|---|
| フルメタデータ差分 | 完全監査、規制遵守 | カスタムタグを含むすべてのプロパティを取得 | 非常に大きなファイルではメモリ使用量が増える |
| 所有権変更検出 | 法的所有権争い、e‑discovery | 人物関連フィールドに絞り、読みやすい | 他の有用なメタデータは無視される |
| リビジョン履歴検出 | タイムラインフォレンジック、変更ログ検証 | タイムスタンプとリビジョン番号に特化 | コンテンツレベルの変更は示さない |
コンプライアンス目標に合わせて手法を選択してください。多くの場合、フル差分を実行した後に所有権やリビジョンセクションに絞って掘り下げる組み合わせが最も有益です。
差分のエクスポート
MetadataDiff を取得したら、結果を共有する必要が出てきます。以下にシンプルな CSV と JSON エクスポーターを示します。
CSV エクスポート
StringBuilder sb = new StringBuilder();
sb.append("change_type,property,old_value,new_value\n");
for (Map.Entry<String, String> e : diff.added.entrySet()) {
sb.append("added,").append(esc(e.getKey()))
.append(",,").append(esc(e.getValue())).append("\n");
}
for (Map.Entry<String, String> e : diff.removed.entrySet()) {
sb.append("removed,").append(esc(e.getKey()))
.append(",").append(esc(e.getValue()))
.append(",\n");
}
for (Map.Entry<String, String[]> e : diff.changed.entrySet()) {
sb.append("changed,").append(esc(e.getKey()))
.append(",").append(esc(e.getValue()[0]))
.append(",").append(esc(e.getValue()[1])).append("\n");
}
Files.write(Paths.get(outputPath), sb.toString().getBytes(StandardCharsets.UTF_8));
JSON エクスポート
StringBuilder sb = new StringBuilder();
sb.append("{\n");
sb.append(" \"added\": {\n");
writeMap(sb, diff.added);
sb.append(" },\n");
sb.append(" \"removed\": {\n");
writeMap(sb, diff.removed);
sb.append(" },\n");
sb.append(" \"changed\": {\n");
int i = 0;
for (Map.Entry<String, String[]> e : diff.changed.entrySet()) {
String comma = ++i < diff.changed.size() ? "," : "";
sb.append(" \"").append(escape(e.getKey()))
.append("\": { \"from\": \"")
.append(escape(e.getValue()[0])).append("\", \"to\": \"")
.append(escape(e.getValue()[1])).append("\" }")
.append(comma).append("\n");
}
sb.append(" }\n");
sb.append("}\n");
Files.write(Paths.get(outputPath), sb.toString().getBytes(StandardCharsets.UTF_8));
どちらのエクスポーターも、カンマや引用符を安全に処理するヘルパーメソッド(esc、escape、writeMap)に依存しています。
ベストプラクティスとヒント
- 差分のスコープを限定: 大容量 PDF の場合は所有権またはリビジョンタグだけに絞って処理時間を短縮します。
- ファイル形式の検証: プロパティを走査する前に必ず
metadata.getFileFormat() != FileFormat.Unknownを確認してください。 - リソースの解放:
try (Metadata metadata = new Metadata(path)) { … }の try‑with‑resources を使用してネイティブハンドルを解放します。 - バージョンの一貫性: 両文書が同一フォーマットバージョンであることを確認してください。DOCX と古い DOC を混在させると誤った結果になる可能性があります。
- セキュリティ: 生のメタデータ値を公開 API でそのまま返さないでください。CSV/JSON 書き出し時は必ず
esc/escapeヘルパーでサニタイズしましょう。 - パフォーマンス: 大量取り込みには CSV、ヒューマンリーダブルな監査ログには JSON を選択すると効果的です。
結論
GroupDocs.Metadata for Java はメタデータフォレンジックをシンプルに実現します。フル差分、所有権検出、リビジョン履歴分析の各手法を組み合わせることで、隠れた変更を可視化し、法的証拠として活用でき、コンプライアンス要件も満たす堅牢な監査パイプラインを構築できます。CSV や JSON へのエクスポートにより、レポーティングツールやデータウェアハウスへのシームレスな統合が可能です。
次のステップ:
- カスタムメタデータを絞り込む高度なタグ仕様を調査してください GroupDocs.Metadata Java docs。
- バッチで複数文書を比較する方法を学びましょう API reference。
- エンドツーエンド実装の公式サンプルプロジェクトを確認してください GitHub examples。