Introduction
هنگامی که تیمهای حقوقی یا تحلیلگران جرمشناسی نیاز دارند ثابت کنند یک سند دستکاری نشده است، فقط نگاه کردن به محتوای قابل مشاهده کافی نیست. ویژگیهای مخفی—مانند نویسنده، تاریخ ایجاد یا شماره نسخه—میتوانند نشان دهند چه کسی چه زمانی به فایل دست زده است. شناسایی این تغییرات جزئی در نسخههای مختلف سند یک نقطه درد رایج است که اغلب نیاز به بررسی دستی هر ویژگی دارد، کاری زمانبر و مستعد خطا.
GroupDocs.Metadata برای Java یک روش برنامهنویسی برای استخراج تمام فیلدهای متادیتا و محاسبه تفاوت ساختاری بین دو نسخه فراهم میکند. در این آموزش سه رویکرد عملی را مقایسه میکنیم: تفاوت کامل متادیتا، تشخیص تغییرات مالکیت متمرکز، و تحلیل تاریخچه نسخهها. هر روش با کدهای کوتاه، قابل کپی‑پیست، نشان داده میشود و همچنین نحوه خروجی گرفتن نتایج به CSV یا JSON برای گزارشگیری حسابرسی آورده شده است.
من این مشکل را هنگام بررسی قراردادی که توسط چندین طرف در طول ماهها ویرایش شده بود، تجربه کردم؛ متن قابل مشاهده یکسان به نظر میرسید، اما فیلدهای مالکیت بهصورت ساکن تغییر کرده بودند.
How can I tell which metadata fields changed between two document versions?
GroupDocs.Metadata هر فایل را بارگذاری میکند، تمام ویژگیهای قابل دسترسی را به یک نقشه (map) استخراج میکند و سپس بر روی کلیدها تکرار میکند تا افزودنها، حذفها و تغییرات را طبقهبندی کند. این کتابخانه برچسبهای داخلی و سفارشی را مدیریت میکند، بنابراین بدون نوشتن پارسرهای مخصوص فرمت، تصویر کاملی به دست میآورید. نتیجه یک شیء MetadataDiff است که میتوانید آن را پرسوجو یا برای گزارشهای انطباق سریالسازی کنید.
Prerequisites
- Java 8 یا بالاتر
- GroupDocs.Metadata برای Java 24.7 (temporary license)
- دو فایل سند که میخواهید مقایسه کنید (مثلاً
contract_v1.pdfوcontract_v2.pdf)
Installation
افزودن وابستگی از طریق Maven:
<dependency>
<groupId>com.groupdocs</groupId>
<artifactId>groupdocs-metadata</artifactId>
<version>24.7</version>
</dependency>
Method 1 – Full Metadata Diff
این روش تمام ویژگیهای متادیتا را از هر دو نسخه استخراج میکند و ورودیهای اضافهشده، حذفشده و تغییر یافته را گزارش میدهد.
// 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;
Key points:
- Comprehensive: Captures all tags, including custom ones.
- Simple map logic: No external diff library required.
- Result object:
added,removed, andchangedmaps are ready for further processing.
💡 Tip: Use this when you need a full audit trail for regulatory compliance.
Method 2 – Detect Ownership Changes
اختلافات حقوقی اغلب به این بستگی دارد که چه کسی سند را ایجاد یا ویرایش کرده است. این روش بر برچسبهای مرتبط با افراد مانند 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;
Key points:
- Targeted: Only identity‑bearing properties are examined.
- Clear output: Returns a map where each entry shows
[old, new]values. - Compliance‑ready: Perfect for e‑discovery or contract‑ownership disputes.
💡 Tip: Combine this with the full diff if you need both breadth and depth.
Method 3 – Detect Revision History Changes
شمارههای نسخه، زمانهای ویرایش و تاریخهای چاپ برای کاربران نهایی نامرئی هستند اما برای زمانبندی جرمشناسی حیاتیاند. این روش بر برچسبهای مرتبط با زمان تمرکز میکند.
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;
Key points:
- Timeline reconstruction: Shows how many times a file was edited or printed.
- Numeric deltas: Useful for detecting suspicious rapid revisions.
- Lightweight: Only three tags are queried, keeping execution fast.
💡 Tip: Use this when you need to prove a document was not altered after a specific deadline.
Comparing Methods: When to Use Each
| Method | Best For | Key Advantages | Limitations |
|---|---|---|---|
| Full Metadata Diff | Full audit, regulatory compliance | Captures every property, including custom tags | Larger memory footprint for very big files |
| Ownership Change Detection | Legal ownership disputes, e‑discovery | Focuses on person‑related fields, easy to read | Ignores other useful metadata |
| Revision History Detection | Timeline forensics, change‑log verification | Isolates timestamps and revision numbers | Does not show content‑level changes |
روش مناسب را بر اساس هدف انطباق خود انتخاب کنید. در بسیاری از موارد ترکیبی—اجرای diff کامل و سپس تمرکز بر بخشهای مالکیت یا نسخه—بیشترین بینش را فراهم میکند.
Exporting the Diff
پس از بهدست آوردن یک MetadataDiff، اغلب نیاز به اشتراکگذاری نتایج دارید. در ادامه دو خروجیساز ساده آورده شده است.
CSV Export
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 Export
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) که بهصورت ایمن کاما و علامتهای نقل قول را مدیریت میکنند، وابستهاند.
Best Practices and Tips
- Scope your diff: برای PDFهای بزرگ، diff را به برچسبهای مالکیت یا نسخه محدود کنید تا زمان پردازش کاهش یابد.
- Validate file format: همیشه قبل از پیمایش ویژگیها
metadata.getFileFormat() != FileFormat.Unknownرا بررسی کنید. - Dispose resources: از
try‑with‑resources(try (Metadata metadata = new Metadata(path)) { … }) برای آزادسازی دستگیرههای بومی استفاده کنید. - Version consistency: اطمینان حاصل کنید هر دو سند از همان نسخه فرمت فایل هستند؛ ترکیب DOCX و DOC قدیمی میتواند نتایج گمراهکننده بدهد.
- Security: مقادیر خام متادیتا را بدون پاکسازی در APIهای عمومی منتشر نکنید؛ هنگام نوشتن CSV/JSON از کمککنندههای
esc/escapeاستفاده کنید. - Performance: برای بارگذاری انبوه به CSV خروجی دهید؛ JSON برای لاگهای حسابرسی قابل خواندن توسط انسان مناسبتر است.
Conclusion
GroupDocs.Metadata برای Java تجزیه و تحلیل متادیتا را ساده میکند. با بهرهگیری از روشهای diff کامل، تشخیص مالکیت و تحلیل تاریخچه نسخه میتوانید یک خط لوله حسابرسی قوی بسازید که تغییرات مخفی را آشکار میسازد، شواهد قانونی را پشتیبانی میکند و الزامات انطباق را برآورده میسازد. خروجی به CSV یا JSON یکپارچهسازی بیدردسر با ابزارهای گزارشگیری یا پایانههای دادهانبار را امکانپذیر میسازد.
Next steps:
- Explore advanced tag specifications to filter custom metadata GroupDocs.Metadata Java docs.
- Learn how to compare multiple documents in a batch API reference.
- Check out the official sample projects for end‑to‑end implementations GitHub examples.