Skip to content

Commit 6e84838

Browse files
authored
TIKA-4797 add migration table (#2978)
1 parent c87f1ae commit 6e84838

16 files changed

Lines changed: 1262 additions & 9 deletions

File tree

docs/modules/ROOT/pages/migration-to-4x/metadata-changes-4x.adoc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,13 @@ String value = metadata.get("mapi:some-property");
307307
String value = metadata.get("mapi:some-property");
308308
----
309309

310+
[CAUTION]
311+
====
312+
`pst:folderPath` does not migrate one-to-one, and the opt-in `LegacyKeyMigrationFilter` cannot
313+
restore it: it is *dropped*, not renamed. In 4.x the Outlook PST parser folds the folder path into
314+
`tk:internal-path` (`<folder>/<item-name>`, a superset), so there is no folder-only value to map back to.
315+
====
316+
310317
=== Resource Name
311318

312319
[source,java]

tika-core/pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@
207207
<inputExclude>src/test/resources/test-documents/file-list.txt</inputExclude>
208208
<inputExclude>src/test/resources/test-documents/ang20150420t182050_corr_v1e_img.hdr</inputExclude>
209209
<inputExclude>src/test/resources/test-documents/*.pdf</inputExclude>
210+
<inputExclude>src/main/resources/org/apache/tika/metadata/metadata-migration-3x-4x.json</inputExclude>
210211
</inputExcludes>
211212
</configuration>
212213
</plugin>

tika-core/src/main/java/org/apache/tika/metadata/MAPI.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ public interface MAPI {
4949

5050
Property SUBMISSION_ACCEPTED_AT_TIME = Property.internalDate(PREFIX_MAPI_META + "msg-submission-accepted-at-time");
5151

52+
// PidTagClientSubmitTime -- distinct MAPI prop from the provider-side SUBMISSION_ACCEPTED_AT_TIME.
53+
Property CLIENT_SUBMIT_TIME = Property.internalDate(PREFIX_MAPI_META + "msg-client-submit-time");
54+
5255
Property SUBMISSION_ID = Property.internalText(PREFIX_MAPI_META + "msg-submission-id");
5356

5457
Property INTERNET_MESSAGE_ID = Property.internalText(PREFIX_MAPI_META + "internet-message-id");
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.tika.metadata.filter;
18+
19+
import java.io.InputStream;
20+
import java.nio.charset.StandardCharsets;
21+
import java.util.ArrayList;
22+
import java.util.HashMap;
23+
import java.util.HashSet;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.Set;
27+
import java.util.function.UnaryOperator;
28+
import java.util.regex.Matcher;
29+
import java.util.regex.Pattern;
30+
31+
import org.apache.tika.annotation.TikaComponent;
32+
import org.apache.tika.metadata.Metadata;
33+
import org.apache.tika.metadata.TikaCoreProperties;
34+
35+
/**
36+
* Rewrites 4.x metadata keys back to their 3.x spellings (or vice versa) so consumers pinned to 3.x
37+
* key names keep working after the 4.0 rename break. Opt-in; enable it in the metadata-filter chain
38+
* and it rewrites at the emit edge. Because it runs inside the {@link MetadataFilter} trusted bracket,
39+
* it may write reserved (e.g. {@code X-TIKA:}/{@code tk:}) keys that a plain String write couldn't.
40+
*
41+
* <p><b>Data-driven, not hand-coded.</b> The renames come from the committed migration table
42+
* (generated by joining the 3.x and 4.x field-attributed key tables — TIKA-4797). Three tiers:
43+
* <ul>
44+
* <li><b>flat renames</b> — the enumerated key-for-key changes (the bulk);</li>
45+
* <li><b>drops</b> — 3.x keys 4.x no longer emits (only relevant on ingest);</li>
46+
* <li><b>prefix rules</b> — the open families whose suffix also transforms
47+
* ({@code tk:exception:}/{@code tk:warn:} snake&harr;kebab, {@code tk:digest:} algorithm names).</li>
48+
* </ul>
49+
* Unmapped keys pass through unchanged (this is a compatibility bridge, not an allow-list).
50+
*
51+
* <p><b>Skeleton status (TIKA-4799):</b> the flat/drop/passthrough mechanism is complete; the
52+
* bundled table and the concrete prefix rules land with the rename PRs (the table resource is absent
53+
* until then, so the default filter is a safe no-op). See {@link #prefixRules} for where the
54+
* digest/exception rules slot in.
55+
*/
56+
@TikaComponent
57+
public class LegacyKeyMigrationFilter extends MetadataFilterBase {
58+
59+
/** V4_TO_V3 = egress (4.x output rewritten to 3.x keys, the BWC case); V3_TO_V4 = ingest. */
60+
public enum Direction { V4_TO_V3, V3_TO_V4 }
61+
62+
/** Generated by the TIKA-4797 migration join; deposited into tika-core resources when renames land. */
63+
static final String DEFAULT_TABLE = "/org/apache/tika/metadata/metadata-migration-3x-4x.json";
64+
65+
// {"v3":"...","v4":"..."} ; v4 == "DROPPED" marks a 3.x key with no 4.x successor.
66+
private static final Pattern ROW =
67+
Pattern.compile("\\{\"v3\":\"(.*?)\",\"v4\":\"(.*?)\"}");
68+
69+
// 4.x digest keys use the JCA name (DigestDef.getJavaName); 3.x used the enum name. MD2/MD5 match.
70+
private static final Map<String, String> DIGEST_ALG_V4_TO_V3 = Map.of(
71+
"SHA-1", "SHA1", "SHA-256", "SHA256", "SHA-384", "SHA384", "SHA-512", "SHA512",
72+
"SHA3-256", "SHA3_256", "SHA3-384", "SHA3_384", "SHA3-512", "SHA3_512");
73+
private static final String V4_DIGEST = TikaCoreProperties.TIKA_META_PREFIX + "digest:";
74+
private static final String V3_DIGEST = TikaCoreProperties.LEGACY_TIKA_META_PREFIX + "digest:";
75+
76+
public static class Config {
77+
public Direction direction = Direction.V4_TO_V3;
78+
public String table = DEFAULT_TABLE;
79+
}
80+
81+
private final Map<String, String> renames; // source key -> target key (direction applied)
82+
private final Set<String> drops; // source keys to remove (ingest only)
83+
private final List<UnaryOperator<String>> prefixRules;
84+
85+
public LegacyKeyMigrationFilter() {
86+
this(new Config());
87+
}
88+
89+
public LegacyKeyMigrationFilter(Config config) {
90+
this(loadTable(config.table), config.direction);
91+
}
92+
93+
/** Test/explicit hook. {@code table} is v3-&gt;v4 (with {@code null}/"DROPPED" = dropped). */
94+
LegacyKeyMigrationFilter(Map<String, String> table, Direction direction) {
95+
this.renames = new HashMap<>();
96+
this.drops = new HashSet<>();
97+
for (Map.Entry<String, String> e : table.entrySet()) {
98+
String v3 = e.getKey();
99+
String v4 = e.getValue();
100+
boolean dropped = v4 == null || "DROPPED".equals(v4);
101+
if (direction == Direction.V4_TO_V3) {
102+
if (!dropped) {
103+
renames.put(v4, v3); // 4.x key -> 3.x key
104+
}
105+
} else { // V3_TO_V4
106+
if (dropped) {
107+
drops.add(v3);
108+
} else {
109+
renames.put(v3, v4);
110+
}
111+
}
112+
}
113+
// Digest keys have no declaring field, so they aren't in the flat table -- rewrite them by
114+
// prefix rule instead. (Exception/warn keys ARE enumerated in the table.)
115+
this.prefixRules = new ArrayList<>();
116+
this.prefixRules.add(digestRule(direction));
117+
}
118+
119+
@Override
120+
protected void filter(Metadata metadata) {
121+
for (String name : metadata.names()) { // names() returns a snapshot copy
122+
if (drops.contains(name)) {
123+
metadata.remove(name);
124+
continue;
125+
}
126+
String mapped = renames.get(name);
127+
if (mapped == null) {
128+
for (UnaryOperator<String> rule : prefixRules) {
129+
String r = rule.apply(name);
130+
if (r != null) {
131+
mapped = r;
132+
break;
133+
}
134+
}
135+
}
136+
if (mapped != null && !mapped.equals(name)) {
137+
String[] values = metadata.getValues(name);
138+
metadata.remove(name);
139+
for (String v : values) {
140+
metadata.add(mapped, v); // trusted context: reserved keys allowed
141+
}
142+
}
143+
// unmapped -> pass through unchanged
144+
}
145+
}
146+
147+
private static Map<String, String> loadTable(String resource) {
148+
Map<String, String> table = new HashMap<>();
149+
try (InputStream in = LegacyKeyMigrationFilter.class.getResourceAsStream(resource)) {
150+
if (in == null) {
151+
return table; // not bundled yet -> no-op bridge
152+
}
153+
Matcher m = ROW.matcher(new String(in.readAllBytes(), StandardCharsets.UTF_8));
154+
while (m.find()) {
155+
table.put(unescape(m.group(1)), unescape(m.group(2)));
156+
}
157+
} catch (Exception e) {
158+
throw new IllegalStateException("could not read migration table " + resource, e);
159+
}
160+
return table;
161+
}
162+
163+
private static String unescape(String s) {
164+
return s.replace("\\\"", "\"").replace("\\\\", "\\");
165+
}
166+
167+
/** {@code tk:digest:<jca-alg>[:enc]} &harr; {@code X-TIKA:digest:<enum-alg>[:enc]} (encoding kept). */
168+
private static UnaryOperator<String> digestRule(Direction direction) {
169+
boolean egress = direction == Direction.V4_TO_V3;
170+
String fromPrefix = egress ? V4_DIGEST : V3_DIGEST;
171+
String toPrefix = egress ? V3_DIGEST : V4_DIGEST;
172+
Map<String, String> alg = new HashMap<>();
173+
DIGEST_ALG_V4_TO_V3.forEach((v4, v3) -> alg.put(egress ? v4 : v3, egress ? v3 : v4));
174+
return name -> {
175+
if (!name.startsWith(fromPrefix)) {
176+
return null;
177+
}
178+
String rest = name.substring(fromPrefix.length()); // <alg> or <alg>:<enc>
179+
int c = rest.indexOf(':');
180+
String a = c < 0 ? rest : rest.substring(0, c);
181+
String enc = c < 0 ? "" : rest.substring(c);
182+
return toPrefix + alg.getOrDefault(a, a) + enc;
183+
};
184+
}
185+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
[
2+
{"v3":"Content-Type-Hint","v4":"tk:content-type-hint"},
3+
{"v3":"Content-Type-Override","v4":"tk:content-type-override"},
4+
{"v3":"Content-Type-Parser-Override","v4":"tk:content-type-parser-override"},
5+
{"v3":"Message:BCC-Display-Name","v4":"message:bcc-display-name"},
6+
{"v3":"Message:BCC-Email","v4":"message:bcc-email"},
7+
{"v3":"Message:BCC-Name","v4":"message:bcc-name"},
8+
{"v3":"Message:CC-Display-Name","v4":"message:cc-display-name"},
9+
{"v3":"Message:CC-Email","v4":"message:cc-email"},
10+
{"v3":"Message:CC-Name","v4":"message:cc-name"},
11+
{"v3":"Message:From-Email","v4":"message:from-email"},
12+
{"v3":"Message:From-Name","v4":"message:from-name"},
13+
{"v3":"Message:To-Display-Name","v4":"message:to-display-name"},
14+
{"v3":"Message:To-Email","v4":"message:to-email"},
15+
{"v3":"Message:To-Name","v4":"message:to-name"},
16+
{"v3":"X-TIKA:EXCEPTION:container_exception","v4":"tk:exception:container-exception"},
17+
{"v3":"X-TIKA:EXCEPTION:embedded_bytes_exception","v4":"tk:exception:embedded-bytes-exception"},
18+
{"v3":"X-TIKA:EXCEPTION:embedded_exception","v4":"tk:exception:embedded-exception"},
19+
{"v3":"X-TIKA:EXCEPTION:embedded_stream_exception","v4":"tk:exception:embedded-stream-exception"},
20+
{"v3":"X-TIKA:EXCEPTION:embedded_warning","v4":"tk:exception:embedded-warning"},
21+
{"v3":"X-TIKA:EXCEPTION:warn","v4":"tk:exception:warn"},
22+
{"v3":"X-TIKA:EXCEPTION:write_limit_reached","v4":"tk:exception:write-limit-reached"},
23+
{"v3":"X-TIKA:Parsed-By","v4":"tk:parsed-by"},
24+
{"v3":"X-TIKA:Parsed-By-Full-Set","v4":"tk:parsed-by-full-set"},
25+
{"v3":"X-TIKA:WARN:truncated_metadata","v4":"tk:warn:truncated-metadata"},
26+
{"v3":"X-TIKA:content","v4":"tk:content"},
27+
{"v3":"X-TIKA:content_handler","v4":"tk:content-handler"},
28+
{"v3":"X-TIKA:detectedEncoding","v4":"tk:detected-encoding"},
29+
{"v3":"X-TIKA:detected_language","v4":"tk:detected-language"},
30+
{"v3":"X-TIKA:detected_language_confidence","v4":"tk:detected-language-confidence"},
31+
{"v3":"X-TIKA:detected_language_confidence_raw","v4":"tk:detected-language-confidence-raw"},
32+
{"v3":"X-TIKA:embedded_depth","v4":"tk:embedded-depth"},
33+
{"v3":"X-TIKA:embedded_id","v4":"tk:embedded-id"},
34+
{"v3":"X-TIKA:embedded_id_path","v4":"tk:embedded-id-path"},
35+
{"v3":"X-TIKA:embedded_resource_path","v4":"tk:embedded-resource-path"},
36+
{"v3":"X-TIKA:encodingDetector","v4":"tk:encoding-detector"},
37+
{"v3":"X-TIKA:encrypted","v4":"tk:encrypted"},
38+
{"v3":"X-TIKA:final_embedded_resource_path","v4":"tk:final-embedded-resource-path"},
39+
{"v3":"X-TIKA:internalPath","v4":"tk:internal-path"},
40+
{"v3":"X-TIKA:origResourceName","v4":"tk:orig-resource-name"},
41+
{"v3":"X-TIKA:parse_time_millis","v4":"tk:parse-time-millis"},
42+
{"v3":"X-TIKA:pipes_result","v4":"tk:pipes-result"},
43+
{"v3":"X-TIKA:sourcePath","v4":"tk:source-path"},
44+
{"v3":"X-TIKA:versionCount","v4":"tk:version-count"},
45+
{"v3":"X-TIKA:versionNumber","v4":"tk:version-number"},
46+
{"v3":"access_permission:assemble_document","v4":"access-permission:assemble-document"},
47+
{"v3":"access_permission:can_modify","v4":"access-permission:can-modify"},
48+
{"v3":"access_permission:can_print","v4":"access-permission:can-print"},
49+
{"v3":"access_permission:can_print_faithful","v4":"access-permission:can-print-faithful"},
50+
{"v3":"access_permission:extract_content","v4":"access-permission:extract-content"},
51+
{"v3":"access_permission:extract_for_accessibility","v4":"access-permission:extract-for-accessibility"},
52+
{"v3":"access_permission:fill_in_form","v4":"access-permission:fill-in-form"},
53+
{"v3":"access_permission:modify_annotations","v4":"access-permission:modify-annotations"},
54+
{"v3":"database:column_count","v4":"database:column-count"},
55+
{"v3":"database:column_name","v4":"database:column-name"},
56+
{"v3":"database:row_count","v4":"database:row-count"},
57+
{"v3":"database:table_name","v4":"database:table-name"},
58+
{"v3":"embeddedResourceType","v4":"tk:embedded-resource-type"},
59+
{"v3":"extended-properties:DocSecurityString","v4":"extended-properties:doc-security-string"},
60+
{"v3":"extended-properties:HiddedSlides","v4":"extended-properties:HiddenSlides"},
61+
{"v3":"hasSignature","v4":"tk:has-signature"},
62+
{"v3":"html_meta:scriptSrc","v4":"html:scriptSrc"},
63+
{"v3":"imagereader:NumImages","v4":"tk:num-images"},
64+
{"v3":"meta:mapi-from-representing-email","v4":"mapi:from-representing-email"},
65+
{"v3":"meta:mapi-from-representing-name","v4":"mapi:from-representing-name"},
66+
{"v3":"meta:mapi-importance","v4":"mapi:importance"},
67+
{"v3":"meta:mapi-is-flagged","v4":"mapi:is-flagged"},
68+
{"v3":"meta:mapi-message-class","v4":"mapi:message-class"},
69+
{"v3":"meta:mapi-msg-client-submit-time","v4":"mapi:msg-client-submit-time"},
70+
{"v3":"meta:mapi-recipients-string","v4":"mapi:recipients-string"},
71+
{"v3":"meta:mapi-sent-by-server-type","v4":"mapi:sent-by-server-type"},
72+
{"v3":"msoffice:ocxName","v4":"msoffice:ocx-name"},
73+
{"v3":"msoffice:progID","v4":"msoffice:prog-id"},
74+
{"v3":"pdf:PDFExtensionVersion","v4":"pdf:pdf-extension-version"},
75+
{"v3":"pdf:PDFVersion","v4":"pdf:pdf-version"},
76+
{"v3":"pdf:actionTrigger","v4":"pdf:action-trigger"},
77+
{"v3":"pdf:actionTriggers","v4":"pdf:action-triggers"},
78+
{"v3":"pdf:actionTypes","v4":"pdf:action-types"},
79+
{"v3":"pdf:annotationSubtypes","v4":"pdf:annotation-subtypes"},
80+
{"v3":"pdf:annotationTypes","v4":"pdf:annotation-types"},
81+
{"v3":"pdf:associatedFileRelationship","v4":"pdf:associated-file-relationship"},
82+
{"v3":"pdf:charsPerPage","v4":"pdf:chars-per-page"},
83+
{"v3":"pdf:containsDamagedFont","v4":"pdf:contains-damaged-font"},
84+
{"v3":"pdf:containsNonEmbeddedFont","v4":"pdf:contains-non-embedded-font"},
85+
{"v3":"pdf:docinfo:creator_tool","v4":"pdf:docinfo:creator-tool"},
86+
{"v3":"pdf:embeddedFileAnnotationType","v4":"pdf:embedded-file-annotation-type"},
87+
{"v3":"pdf:embeddedFileDescription","v4":"pdf:embedded-file-description"},
88+
{"v3":"pdf:embeddedFileSubtype","v4":"pdf:embedded-file-subtype"},
89+
{"v3":"pdf:eofOffsets","v4":"pdf:eof-offsets"},
90+
{"v3":"pdf:has3D","v4":"pdf:has-3d"},
91+
{"v3":"pdf:hasAcroFormFields","v4":"pdf:has-acro-form-fields"},
92+
{"v3":"pdf:hasCollection","v4":"pdf:has-collection"},
93+
{"v3":"pdf:hasMarkedContent","v4":"pdf:has-marked-content"},
94+
{"v3":"pdf:hasSignatureFields","v4":"pdf:has-signature-fields"},
95+
{"v3":"pdf:hasXFA","v4":"pdf:has-xfa"},
96+
{"v3":"pdf:hasXMP","v4":"pdf:has-xmp"},
97+
{"v3":"pdf:incrementalUpdateCount","v4":"pdf:incremental-update-count"},
98+
{"v3":"pdf:incrementalUpdateNumber","v4":"pdf:incremental-update-number"},
99+
{"v3":"pdf:jsName","v4":"pdf:js-name"},
100+
{"v3":"pdf:num3DAnnotations","v4":"pdf:num-3d-annotations"},
101+
{"v3":"pdf:ocrPageCount","v4":"pdf:ocr-page-count"},
102+
{"v3":"pdf:overallPercentageUnmappedUnicodeChars","v4":"pdf:overall-percentage-unmapped-unicode-chars"},
103+
{"v3":"pdf:totalUnmappedUnicodeChars","v4":"pdf:total-unmapped-unicode-chars"},
104+
{"v3":"pdf:unmappedUnicodeCharsPerPage","v4":"pdf:unmapped-unicode-chars-per-page"},
105+
{"v3":"pdf:xmpLocation","v4":"pdf:xmp-location"},
106+
{"v3":"pdfa:PDFVersion","v4":"pdfa:pdf-version"},
107+
{"v3":"pst:discriptorNodeId","v4":"pst:discriptor-node-id"},
108+
{"v3":"pst:folderPath","v4":"DROPPED"},
109+
{"v3":"pst:isValid","v4":"pst:is-valid"},
110+
{"v3":"rendering:Rendered-By","v4":"tk:rendering:rendered-by"},
111+
{"v3":"rendering:rendering-time-ms","v4":"tk:rendering:rendering-time-ms"},
112+
{"v3":"rtf_meta:contains_encapsulated_html","v4":"rtf:contains-encapsulated-html"},
113+
{"v3":"rtf_meta:emb_app_version","v4":"rtf:embedded-app-version"},
114+
{"v3":"rtf_meta:emb_class","v4":"rtf:embedded-class"},
115+
{"v3":"rtf_meta:emb_item","v4":"rtf:embedded-item"},
116+
{"v3":"rtf_meta:emb_topic","v4":"rtf:embedded-topic"},
117+
{"v3":"rtf_meta:thumbnail","v4":"rtf:thumbnail"},
118+
{"v3":"signature:contact-info","v4":"tk:signature:contact-info"},
119+
{"v3":"signature:date","v4":"tk:signature:date"},
120+
{"v3":"signature:filter","v4":"tk:signature:filter"},
121+
{"v3":"signature:location","v4":"tk:signature:location"},
122+
{"v3":"signature:name","v4":"tk:signature:name"},
123+
{"v3":"signature:reason","v4":"tk:signature:reason"},
124+
{"v3":"tika_pg:page_number","v4":"tk:page:number"},
125+
{"v3":"tika_pg:page_rotation","v4":"tk:page:rotation"},
126+
{"v3":"wordperfect:Build","v4":"quattropro:build"},
127+
{"v3":"wordperfect:Encrypted","v4":"wordperfect:encrypted"},
128+
{"v3":"wordperfect:FileId","v4":"wordperfect:file-id"},
129+
{"v3":"wordperfect:FileSize","v4":"wordperfect:file-size"},
130+
{"v3":"wordperfect:FileType","v4":"wordperfect:file-type"},
131+
{"v3":"wordperfect:Id","v4":"quattropro:id"},
132+
{"v3":"wordperfect:LowestVersion","v4":"quattropro:lowest-version"},
133+
{"v3":"wordperfect:MajorVersion","v4":"wordperfect:major-version"},
134+
{"v3":"wordperfect:MinorVersion","v4":"wordperfect:minor-version"},
135+
{"v3":"wordperfect:ProductType","v4":"wordperfect:product-type"},
136+
{"v3":"wordperfect:Version","v4":"quattropro:version"}
137+
]

0 commit comments

Comments
 (0)