Skip to content

Commit c2ab806

Browse files
authored
TIKA-4797 - Metadata key registry (#2970)
1 parent 65a2f3a commit c2ab806

7 files changed

Lines changed: 989 additions & 0 deletions

File tree

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
<module>tika-ml</module>
4646
<module>tika-encoding-detectors</module>
4747
<module>tika-parsers</module>
48+
<module>tika-metadata-schema</module>
4849
<module>tika-bundles</module>
4950
<module>tika-xmp</module>
5051
<module>tika-langdetect</module>

tika-metadata-schema/README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
# tika-metadata-schema
18+
19+
A machine-readable schema of Apache Tika's metadata keys. Two registries, because Tika has two
20+
kinds of keys:
21+
22+
## `metadata-keys.json` — the closed set (generated + gated)
23+
Every key Tika declares as a `Property` constant, plus the bounded digest cross-product
24+
(`X-TIKA:digest:<ALGORITHM>[:<ENCODING>]`, enumerated from `DigestDef`). Each record:
25+
`{ key, namespace, valueType, cardinality }`.
26+
27+
**Generated, never hand-edited.** `SchemaGenerator` scans the parser classpath for classes that
28+
declare a `Property` field, force-loads them, reads the global `Property` table, and writes stable
29+
sorted JSON. `MetadataSchemaTest` regenerates in-memory and asserts it matches the committed file, so
30+
the registry can never drift from the declarations.
31+
32+
Regenerate after adding/changing a `Property`:
33+
```
34+
java -cp <tika-metadata-schema + deps classpath> \
35+
org.apache.tika.metadata.schema.SchemaGenerator \
36+
src/main/resources/org/apache/tika/metadata/metadata-keys.json
37+
```
38+
39+
## `metadata-open-namespaces.json` — the open sets (curated)
40+
Keys minted at **runtime** whose names are not `Property` constants, so they cannot be generated:
41+
- **open/passthrough namespaces** — file-controlled key names (scraped HTML `<meta>` under `html:`,
42+
OOXML `custom:`, email `Message:Raw-Header:`, Access `MDB_PROP:`, Vorbis comments, GRIB/NetCDF/FLV
43+
attributes, …);
44+
- **templates** — e.g. XMP `rdf:Alt` language variants `<base-key>:<lang>` (`dc:title:fr`).
45+
46+
**Curated (hand-maintained, reviewed), not generated**, and possibly not exhaustive; the
47+
closed-namespace lint (a follow-up) is the intended completeness backstop.
48+
49+
Together the two files describe the whole key space: closed keys are enumerated and gated; open keys
50+
are described by rule.

tika-metadata-schema/pom.xml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
Licensed to the Apache Software Foundation (ASF) under one or more
4+
contributor license agreements. See the NOTICE file distributed with
5+
this work for additional information regarding copyright ownership.
6+
The ASF licenses this file to You under the Apache License, Version 2.0
7+
(the "License"); you may not use this file except in compliance with
8+
the License. You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
-->
18+
<project xmlns="http://maven.apache.org/POM/4.0.0"
19+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
20+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
21+
<modelVersion>4.0.0</modelVersion>
22+
<parent>
23+
<groupId>org.apache.tika</groupId>
24+
<artifactId>tika-parent</artifactId>
25+
<version>${revision}</version>
26+
<relativePath>../tika-parent/pom.xml</relativePath>
27+
</parent>
28+
29+
<artifactId>tika-metadata-schema</artifactId>
30+
<name>Apache Tika metadata schema</name>
31+
<description>Generates and enforces the machine-readable metadata key registry
32+
(metadata-keys.json) from the live Property declarations.</description>
33+
34+
<dependencies>
35+
<dependency>
36+
<groupId>org.apache.tika</groupId>
37+
<artifactId>tika-core</artifactId>
38+
<version>${project.version}</version>
39+
</dependency>
40+
<!-- brings the standard parser classes onto the classpath so their Property
41+
constants are discoverable by the generator -->
42+
<dependency>
43+
<groupId>org.apache.tika</groupId>
44+
<artifactId>tika-parsers-standard-package</artifactId>
45+
<version>${project.version}</version>
46+
<type>pom</type>
47+
</dependency>
48+
49+
<dependency>
50+
<groupId>org.junit.jupiter</groupId>
51+
<artifactId>junit-jupiter</artifactId>
52+
<scope>test</scope>
53+
</dependency>
54+
</dependencies>
55+
<!-- Regenerate the committed registry with:
56+
java -cp <module+deps classpath> org.apache.tika.metadata.schema.SchemaGenerator \
57+
src/main/resources/org/apache/tika/metadata/metadata-keys.json
58+
MetadataSchemaTest regenerates in-memory and asserts no diff, so CI catches drift. -->
59+
</project>
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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.schema;
18+
19+
import java.io.File;
20+
import java.io.IOException;
21+
import java.lang.reflect.Field;
22+
import java.nio.charset.StandardCharsets;
23+
import java.nio.file.Files;
24+
import java.nio.file.Path;
25+
import java.util.Enumeration;
26+
import java.util.Map;
27+
import java.util.TreeMap;
28+
import java.util.jar.JarEntry;
29+
import java.util.jar.JarFile;
30+
31+
import org.apache.tika.digest.DigestDef;
32+
import org.apache.tika.metadata.Property;
33+
34+
/**
35+
* Generates the machine-readable metadata key registry ({@code metadata-keys.json}) from the live
36+
* {@link Property} declarations. Dependency-free: it scans the runtime classpath for classes that
37+
* declare a {@code Property} field, force-loads them (their static init registers into the global
38+
* {@code Property} table), then serializes that table as stable, sorted JSON.
39+
*
40+
* <p>The generated file is committed; {@code MetadataSchemaTest} regenerates and asserts no diff,
41+
* so the registry can never drift from the declarations.
42+
*/
43+
public final class SchemaGenerator {
44+
45+
// Field/parameter descriptor for org.apache.tika.metadata.Property in a .class constant pool.
46+
private static final byte[] PROP_DESC =
47+
"Lorg/apache/tika/metadata/Property;".getBytes(StandardCharsets.ISO_8859_1);
48+
49+
private SchemaGenerator() {
50+
}
51+
52+
/** @return the registry as stable JSON (sorted by key). */
53+
public static String generate() throws Exception {
54+
ClassLoader cl = SchemaGenerator.class.getClassLoader();
55+
for (String entry : System.getProperty("java.class.path").split(File.pathSeparator)) {
56+
File f = new File(entry);
57+
if (f.isDirectory()) {
58+
scanDir(f, f.toPath(), cl);
59+
} else if (f.getName().endsWith(".jar")) {
60+
scanJar(f, cl);
61+
}
62+
}
63+
Field fld = Property.class.getDeclaredField("PROPERTIES");
64+
fld.setAccessible(true);
65+
@SuppressWarnings("unchecked")
66+
Map<String, Property> reg = (Map<String, Property>) fld.get(null);
67+
68+
TreeMap<String, String[]> entries = new TreeMap<>(); // key -> [valueType, cardinality]
69+
for (Map.Entry<String, Property> e : reg.entrySet()) {
70+
entries.put(e.getKey(), new String[]{
71+
e.getValue().getValueType().toString(), e.getValue().getPropertyType().toString()});
72+
}
73+
// Digest keys are a CLOSED cross-product of the supported algorithms and encodings, not an
74+
// open template. Synthesize them via the real DigestDef.metadataKey() so they can't drift.
75+
for (DigestDef.Algorithm a : DigestDef.Algorithm.values()) {
76+
for (DigestDef.Encoding enc : DigestDef.Encoding.values()) {
77+
entries.put(new DigestDef(a, enc).metadataKey(), new String[]{"TEXT", "SIMPLE"});
78+
}
79+
}
80+
return toJson(entries);
81+
}
82+
83+
private static void scanDir(File root, Path dir, ClassLoader cl) throws IOException {
84+
try (var stream = Files.walk(dir)) {
85+
for (Path p : (Iterable<Path>) stream::iterator) {
86+
if (!p.toString().endsWith(".class")) {
87+
continue;
88+
}
89+
String rel = root.toPath().relativize(p).toString().replace(File.separatorChar, '/');
90+
maybeLoad(rel, Files.readAllBytes(p), cl);
91+
}
92+
}
93+
}
94+
95+
private static void scanJar(File jar, ClassLoader cl) throws IOException {
96+
try (JarFile jf = new JarFile(jar)) {
97+
for (Enumeration<JarEntry> e = jf.entries(); e.hasMoreElements(); ) {
98+
JarEntry je = e.nextElement();
99+
if (!je.getName().endsWith(".class")) {
100+
continue;
101+
}
102+
maybeLoad(je.getName(), jf.getInputStream(je).readAllBytes(), cl);
103+
}
104+
}
105+
}
106+
107+
private static void maybeLoad(String classPath, byte[] bytes, ClassLoader cl) {
108+
if (!classPath.startsWith("org/apache/tika/") || !contains(bytes, PROP_DESC)) {
109+
return;
110+
}
111+
String cn = classPath.substring(0, classPath.length() - 6).replace('/', '.');
112+
try {
113+
Class.forName(cn, true, cl); // static init registers any Property constants
114+
} catch (Throwable ignore) {
115+
// classes whose static init needs an absent dependency are skipped; the corpus test backs this
116+
}
117+
}
118+
119+
private static boolean contains(byte[] hay, byte[] needle) {
120+
outer:
121+
for (int i = 0; i <= hay.length - needle.length; i++) {
122+
for (int j = 0; j < needle.length; j++) {
123+
if (hay[i + j] != needle[j]) {
124+
continue outer;
125+
}
126+
}
127+
return true;
128+
}
129+
return false;
130+
}
131+
132+
private static String toJson(TreeMap<String, String[]> entries) {
133+
StringBuilder sb = new StringBuilder("[\n");
134+
int i = 0;
135+
int n = entries.size();
136+
for (Map.Entry<String, String[]> e : entries.entrySet()) {
137+
String k = e.getKey();
138+
String ns = k.contains(":") ? k.substring(0, k.indexOf(':')) : "";
139+
sb.append(" {\"key\":").append(quote(k))
140+
.append(",\"namespace\":").append(quote(ns))
141+
.append(",\"valueType\":\"").append(e.getValue()[0])
142+
.append("\",\"cardinality\":\"").append(e.getValue()[1])
143+
.append("\"}").append(++i < n ? "," : "").append('\n');
144+
}
145+
return sb.append("]\n").toString();
146+
}
147+
148+
private static String quote(String s) {
149+
return '"' + s.replace("\\", "\\\\").replace("\"", "\\\"") + '"';
150+
}
151+
152+
public static void main(String[] args) throws Exception {
153+
Path out = Path.of(args[0]);
154+
Files.writeString(out, generate());
155+
System.out.println("wrote " + out);
156+
}
157+
}

0 commit comments

Comments
 (0)