remove-pii-from-office-metadata-java is a runnable Maven project that clears personal data from Word, Excel, and PowerPoint metadata with GroupDocs.Metadata for Java and then scans the result for anything that survived. Six operations ship in it: four targeted passes, one full sanitize, and a verification scan that separates metadata leaks from leaks living in document body content.
The distinguishing detail on Java is the predicate model. There is no lambda overload on removeProperties, so every rule here is a Specification object, combined through .or(...) chains. That difference shapes the project layout.
Three situations drive most of the traffic to this code. In a pre-publication review, an analyst finishes a board report that still names three internal editors, a manager, and a subsidiary; the identity pass clears those four property groups and leaves Title and Subject intact. In a records export, files pulled off a document server carry approver IDs, workflow paths, and content-type URIs, so the server pass runs before the export leaves the building. In an audit bundle, a compliance officer needs proof rather than a promise, which is what the classified leak report provides.
Office metadata is written by the application, not by the author, so nobody reviews it. Word stamps the account name into Author and LastSavedBy on every save, records TotalEditingTime and LastPrinted, and keeps counters for revisions and comment threads. SharePoint injects its own workflow fields on check-in. None of this appears on any printed page, and all of it travels with the file when it is emailed to an external party.
Removing it by hand does not scale. The Windows properties dialog covers a subset of fields, does nothing for custom OOXML parts, and cannot be scripted across a batch. Teams that try raw OOXML editing end up maintaining XPath expressions against docProps/core.xml and docProps/custom.xml for every format they touch.
The second problem is quieter: a cleanup that silently misses a field looks exactly like a cleanup that worked. Without a read-back step, nobody notices the Manager property that a template re-applied.
- ❌ Identity data spreads across built-in properties, custom parts, and server-injected fields, so one hard-coded name list never covers a real document.
- ❌ Blanket removal destroys useful descriptive metadata such as Title, Subject, and Keywords along with the personal data.
- ❌ Nothing in the toolchain proves the file is clean after the removal step ran.
GroupDocs.Metadata for Java handles the whole set through one property search engine. A Specification decides which properties match, removeProperties deletes everything the specification accepts and returns the affected count, and findProperties runs the same predicate in read-only mode for verification. The API classifies properties with tags, so Tags.getPerson().getCreator() matches author-style fields regardless of which package or format they came from.
Key capabilities used in this project:
✅ Tag specifications – ContainsTagSpecification targets semantic groups (person, corporate) instead of literal names, so one rule works on DOCX, XLSX, and PPTX.
✅ Custom specifications – subclassing Specification gives name-pattern rules for field families the tag system does not classify, such as comment counters.
✅ One-call sanitize – metadata.sanitize() strips every detected metadata package in a single operation and returns how many properties it removed.
✅ Read-back verification – findProperties reuses the removal predicates to prove the result, which is what turns a cleanup into evidence.
- Open the file: construct
Metadatain a try-with-resources block and rejectFileFormat.Unknownbefore touching properties. - Pick the rule shape: tags for identity concepts, name substrings for field families like
Comment*orServer*. - Remove and count:
removePropertiesreturns the affected count, which is the number to log for the audit trail. - Save a copy: write to a new path so the original stays available for dispute resolution.
- Verify: run the leak check against the saved copy and treat a non-empty metadata-leak list as a failed run.
Java has no lambda overload for removeProperties, so name-pattern rules need a specification class. NameContainsSpec accepts a varargs list of substrings and matches any property whose name contains one of them. Three of the six operations reuse it, which keeps the rule text in one file instead of scattered across four call sites.
public class NameContainsSpec extends Specification {
private final String[] needles;
public NameContainsSpec(String... needles) {
this.needles = needles;
}
@Override
public boolean isSatisfiedBy(MetadataProperty candidate) {
String name = candidate.getName();
if (name == null) return false;
for (String n : needles) {
if (name.contains(n)) return true;
}
return false;
}
}- JDK 8 or newer: the project compiles with
maven.compiler.sourceandtargetset to 8. - Maven: dependencies resolve from the GroupDocs Java repository at
https://releases.groupdocs.com/java/repo/. - GroupDocs.Metadata for Java 24.7: pinned in
pom.xml; a missing license file drops the run into evaluation mode instead of failing.
remove-pii-from-office-metadata-java/
│
├── pom.xml
├── output/
│ ├── fully-sanitized.docx
│ ├── no-author.docx
│ ├── no-comments.docx
│ ├── no-revisions.docx
│ └── no-server-props.docx
└── src/
└── main/
├── java/com/groupdocs/samples/removepiioffice/
│ ├── Main.java
│ └── methods/
│ ├── ClearComments.java
│ ├── LeakReport.java
│ ├── NameContainsSpec.java
│ ├── RemoveAuthorAndCompany.java
│ ├── RemoveDocumentServerProperties.java
│ ├── RunLeakCheck.java
│ ├── SanitizeAllPii.java
│ └── StripRevisionHistory.java
└── resources/
└── pii-sample.docx
File Organization:
- pom.xml – pins GroupDocs.Metadata 24.7 and wires the exec plugin to
Main - Main.java – drives all six operations against the sample and asserts each result
- methods/RemoveAuthorAndCompany.java – tag-driven removal of identity properties
- methods/ClearComments.java – clears comment, reviewer, and reviewed fields
- methods/StripRevisionHistory.java – clears revision counters and editing-time trails
- methods/RemoveDocumentServerProperties.java – drops SharePoint and workflow fields
- methods/SanitizeAllPii.java – one-call full metadata wipe
- methods/RunLeakCheck.java – post-cleanup scan that classifies residual PII
- methods/NameContainsSpec.java and methods/LeakReport.java – the shared substring specification and the two-list result carrier
- src/main/resources/pii-sample.docx – seeded input carrying every PII group
- output/ – the five cleaned DOCX files a full run writes
Use it when Run this first, on any document heading outside the organization, when the descriptive metadata still has to survive.
Tag specifications target the core identity-bearing properties Word, Excel, and PowerPoint write on every save. Four tags cover the group: creator, editor, manager, and the corporate company field. Because tags classify meaning rather than spelling, the same predicate keeps working when a property arrives from a different metadata package. This is the first step of a GDPR or ISO 27001 pre-publication workflow.
try (Metadata metadata = new Metadata(inputPath)) {
if (metadata.getFileFormat() == FileFormat.Unknown) return 0;
int affected = metadata.removeProperties(
new ContainsTagSpecification(Tags.getPerson().getCreator())
.or(new ContainsTagSpecification(Tags.getPerson().getEditor()))
.or(new ContainsTagSpecification(Tags.getPerson().getManager()))
.or(new ContainsTagSpecification(Tags.getCorporate().getCompany())));
metadata.save(outputPath);
return affected;
}The names of everyone who touched the file disappear while Title, Subject, and Keywords stay usable for search and records management.
In practice: A draft contract goes to opposing counsel without disclosing which associate wrote it or which internal template it started from.
Use it when Publishing minutes, policies, or reports that went through an internal review round.
The comment group is not classified by tags, so the rule matches property names containing Comment, Reviewer, or Reviewed. Those fields carry reviewer names, review timestamps, and counters that reveal how much argument a document caused. Run it after the identity pass when preparing a document for external distribution.
try (Metadata metadata = new Metadata(inputPath)) {
if (metadata.getFileFormat() == FileFormat.Unknown) return 0;
int affected = metadata.removeProperties(
new NameContainsSpec("Comment", "Reviewer", "Reviewed"));
metadata.save(outputPath);
return affected;
}Review traces stop travelling with the published file, and the affected count tells you how many comment-related properties were actually present.
In practice: Board minutes reach a shareholder portal with the review-thread metadata gone.
Use it when The document is fine to share but the editing history is not.
Revision, TrackedChange, LastPrinted, TotalEditingTime, and EditTime together reconstruct a timeline: how many drafts existed, who edited them, how long the work took, and when the file was last printed. Those five substrings sit in one specification instance, so the whole timeline group clears in a single pass.
try (Metadata metadata = new Metadata(inputPath)) {
if (metadata.getFileFormat() == FileFormat.Unknown) return 0;
int affected = metadata.removeProperties(new NameContainsSpec(
"Revision", "TrackedChange", "LastPrinted", "TotalEditingTime", "EditTime"));
metadata.save(outputPath);
return affected;
}The edit trail stops being readable from file properties, which matters when the number of revisions is itself sensitive.
In practice: A regulatory filing ships without exposing that it was rewritten eleven times in the final week.
Use it when Any file that has lived on a document server or content management system.
Server-managed properties carry internal workflow paths, approver identifiers, and content-type URIs that describe the organization behind the document. The rule matches names containing Server, Workflow, Approver, ContentType, or Template. Field names vary between server versions, so check the affected count against your own files rather than assuming the list is complete.
try (Metadata metadata = new Metadata(inputPath)) {
if (metadata.getFileFormat() == FileFormat.Unknown) return 0;
int affected = metadata.removeProperties(new NameContainsSpec(
"Server", "Workflow", "Approver", "ContentType", "Template"));
metadata.save(outputPath);
return affected;
}Internal site structure, approval chains, and template origins stop leaking through properties nobody opens.
In practice: A tender response leaves the company without disclosing the internal approval chain that signed it off.
Use it when At the trust boundary, when no metadata needs to survive.
metadata.sanitize() strips every detected metadata package: document-info identity fields, comments, revision history, tracked-change authors, and custom OOXML parts. It is more thorough than any name-based predicate, and it is also blunt, since Title and Subject go with everything else. The return value is the number of properties removed, which belongs in the audit log.
try (Metadata metadata = new Metadata(inputPath)) {
if (metadata.getFileFormat() == FileFormat.Unknown) return 0;
int affected = metadata.sanitize();
metadata.save(outputPath);
return affected;
}One call replaces four predicates when the requirement is total cleanup rather than selective cleanup.
In practice: Files published to an open data portal are wiped in a single step before upload.
Use Case: Scans a sanitized document for residual PII and splits metadata leaks from content-level leaks
Use it when After every removal pass, on the saved copy rather than the source.
The scan reuses the same tag and name rules through findProperties, so verification cannot drift away from the removal logic. Properties are then sorted into two lists, and metadata leaks must be empty for the document to count as clean. The first block builds the combined predicate:
LeakReport report = new LeakReport();
try (Metadata metadata = new Metadata(path)) {
if (metadata.getFileFormat() == FileFormat.Unknown) return report;
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()))
.or(new NameContainsSpec(
"Comment", "Reviewer", "Revision", "TrackedChange",
"Classification", "Department", "Server", "Workflow")))) {Empty values and zero counters are skipped so a cleared counter does not read as a leak. Names starting with Comment, Revision, or Inspection are wrapper entries over body content, which is why they land in a separate list:
String value = "";
if (p.getValue() != null && p.getValue().getRawValue() != null) {
value = String.valueOf(p.getValue().getRawValue());
}
if (value.isEmpty() || value.equals("0") || value.equals("0.0")) continue;
String entry = p.getName() + "=" + value;
String name = p.getName() == null ? "" : p.getName();
if (name.startsWith("Comment") || name.startsWith("Revision")
|| name.startsWith("Inspection")) {
report.contentLevelLeaks.add(entry);
} else {
report.metadataLeaks.add(entry);
}
}
}
return report;The split keeps the pass/fail signal honest. GroupDocs.Metadata works on metadata packages, so Word comments and tracked-change authors stored inside word/document.xml are reported but not removed; clearing those needs a content-editing library such as Aspose.Words.
In practice: A nightly sanitization job fails the build when metadataLeaks is non-empty and files an informational note when only content-level entries remain.
Tag specifications survive the move from DOCX to XLSX and PPTX without a rewrite, so one predicate model covers four formats. Every operation returns an affected count, which lets a log line state what was removed instead of that something was attempted. The same API covers both a surgical pass during collaboration and a full wipe at the boundary. And because findProperties reuses the removal specifications, the verification step cannot quietly diverge from the cleanup it checks.
I wrote the leak check before any of the removal passes, because an earlier version of this workflow reported success on a file that still carried a Manager value re-applied by a template.
If you are building document sanitization or metadata compliance tooling in Java, these resources will help you:
-
Step-by-step use case guide in the documentation – The full walkthrough of the six operations, including when a targeted pass beats a full sanitize: Read the article →
-
In-depth blog article about this project – Business context for metadata PII, plus the reasoning behind splitting metadata leaks from content-level leaks: Read the article →
-
How to Work with Metadata Tags: Semantic Property Targeting Across Formats – Explains the tag system
ContainsTagSpecificationqueries, with examples of person, corporate, and time tags: Read the article → -
Metadata Scrubbing: Online and Programmatic Approaches to Clean Document Properties – Compares interactive scrubbing with API-driven removal and where each fits a records workflow: Read the article →
-
Removing Metadata Properties with Search Specifications in Java – Reference page for
removePropertiesand the specification types this project builds on: Read the article → -
Cleaning Metadata Packages: What sanitize() Touches – Documents the scope of a full sanitize and what it leaves behind: Read the article →
remove pii, document sanitization, office metadata, java metadata api, metadata leak check, gdpr documents, clean docx properties, tracked changes cleanup, sharepoint workflow properties, groupdocs metadata, removeProperties, sanitize, ContainsTagSpecification, Specification, findProperties, docx, xlsx, pptx, document privacy, iso 27001, metadata scrubbing, maven, document server properties, author metadata removal
Ready to get started? View Documentation | Get Support | Request License