This is an automated email from the ASF dual-hosted git repository.

tballison pushed a commit to branch TIKA-4816-metadata-key-api
in repository https://gitbox.apache.org/repos/asf/tika.git

commit ff467d33c5233669837e256b2de2eda45d24b7b0
Author: tallison <[email protected]>
AuthorDate: Tue Aug 11 12:29:22 2026 -0400

    TIKA-4816 metadata-key stage 2: mint-time reserved-name validation
---
 .../apache/tika/digest/InputStreamDigester.java    |  16 +-
 .../java/org/apache/tika/metadata/Property.java    | 171 +++++++++++++++++++++
 .../java/org/apache/tika/metadata/Rendering.java   |  18 ++-
 .../apache/tika/metadata/TikaCoreProperties.java   | 142 ++++++++++-------
 .../org/apache/tika/metadata/TikaPagedText.java    |   6 +-
 .../java/org/apache/tika/utils/ParserUtils.java    |   7 +-
 .../tika/metadata/PropertyReservedNameTest.java    | 161 +++++++++++++++++++
 .../org/apache/tika/eval/app/ProfilerBase.java     |   7 +-
 .../apache/tika/metadata/metadata-key-fields.json  |   3 +
 .../tika/renderer/pdf/pdfbox/PDFBoxRenderer.java   |  12 +-
 10 files changed, 474 insertions(+), 69 deletions(-)

diff --git 
a/tika-core/src/main/java/org/apache/tika/digest/InputStreamDigester.java 
b/tika-core/src/main/java/org/apache/tika/digest/InputStreamDigester.java
index 849b54a833..842746c141 100644
--- a/tika-core/src/main/java/org/apache/tika/digest/InputStreamDigester.java
+++ b/tika-core/src/main/java/org/apache/tika/digest/InputStreamDigester.java
@@ -26,6 +26,7 @@ import java.util.concurrent.ConcurrentHashMap;
 import org.apache.tika.io.TikaInputStream;
 import org.apache.tika.metadata.Metadata;
 import org.apache.tika.metadata.Property;
+import org.apache.tika.metadata.TikaCoreProperties;
 import org.apache.tika.parser.ParseContext;
 import org.apache.tika.utils.StringUtils;
 
@@ -35,6 +36,9 @@ import org.apache.tika.utils.StringUtils;
  */
 public class InputStreamDigester implements Digester {
 
+    private static final String DIGEST_KEY_PREFIX =
+            TikaCoreProperties.TIKA_META_PREFIX + "digest" + 
TikaCoreProperties.NAMESPACE_PREFIX_DELIMITER;
+
     private static final Map<String, Property> METADATA_KEY_PROPERTIES = new 
ConcurrentHashMap<>();
 
     private final String algorithm;
@@ -51,10 +55,20 @@ public class InputStreamDigester implements Digester {
     public InputStreamDigester(String algorithm, String metadataKey, Encoder 
encoder) {
         this.algorithm = algorithm;
         this.metadataProperty =
-                METADATA_KEY_PROPERTIES.computeIfAbsent(metadataKey, 
Property::internalText);
+                METADATA_KEY_PROPERTIES.computeIfAbsent(metadataKey, 
InputStreamDigester::mintProperty);
         this.encoder = encoder;
     }
 
+    // metadataKey is config-supplied (algorithm/encoding names), not 
document-derived; the
+    // usual tk:digest:* shape mints via the curated template factory (stays 
registered, so
+    // Metadata.reconstruct/Property.get resolve it), anything else keeps the 
public factory.
+    private static Property mintProperty(String metadataKey) {
+        if (metadataKey.startsWith(DIGEST_KEY_PREFIX)) {
+            return 
TikaCoreProperties.digestProperty(metadataKey.substring(DIGEST_KEY_PREFIX.length()));
+        }
+        return Property.internalText(metadataKey);
+    }
+
     private static void setContentLength(long length, Metadata metadata) {
         if (StringUtils.isBlank(metadata.get(Metadata.CONTENT_LENGTH))) {
             //only add it if it hasn't been populated already
diff --git a/tika-core/src/main/java/org/apache/tika/metadata/Property.java 
b/tika-core/src/main/java/org/apache/tika/metadata/Property.java
index aa3df83307..da138f3a96 100644
--- a/tika-core/src/main/java/org/apache/tika/metadata/Property.java
+++ b/tika-core/src/main/java/org/apache/tika/metadata/Property.java
@@ -136,6 +136,34 @@ public final class Property implements 
Comparable<Property> {
         return new Property(name, internal, propertyType, valueType, choices, 
null, null, false);
     }
 
+    /**
+     * Guards the public factories: a {@code tk:}/{@code X-TIKA:} name can 
never be minted
+     * through them. Curated reserved constants go through the package-private
+     * {@code reservedInternal*}/{@code reservedExternal*} family below 
instead, so any live
+     * {@code Property} carrying a reserved name is, by construction, curated.
+     */
+    private static void requireNotReserved(String name) {
+        if (ReservedNamespaces.isTikaNative(name)) {
+            throw new IllegalArgumentException("'" + name + "' is in the 
reserved Tika-native "
+                    + "namespace (tk:/X-TIKA:); it cannot be minted via a 
public Property "
+                    + "factory. Curated tk: constants belong in 
org.apache.tika.metadata and "
+                    + "mint via the package-private reserved* factories.");
+        }
+    }
+
+    /**
+     * Mirror image of {@link #requireNotReserved(String)} for the reserved 
factories below:
+     * fail loud (rather than silently minting a non-reserved Property through 
the reserved
+     * path) if fed a name that isn't actually reserved.
+     */
+    private static void requireReserved(String name) {
+        if (!ReservedNamespaces.isTikaNative(name)) {
+            throw new IllegalArgumentException("'" + name + "' is not in the 
reserved "
+                    + "Tika-native namespace (tk:/X-TIKA:); use a public 
Property factory "
+                    + "instead of the package-private reserved* path.");
+        }
+    }
+
     /**
      * Get the type of a property
      *
@@ -175,90 +203,229 @@ public final class Property implements 
Comparable<Property> {
     }
 
     public static Property internalBoolean(String name) {
+        requireNotReserved(name);
         return new Property(name, true, ValueType.BOOLEAN);
     }
 
     public static Property internalClosedChoise(String name, String... 
choices) {
+        requireNotReserved(name);
         return new Property(name, true, ValueType.CLOSED_CHOICE, choices);
     }
 
     public static Property internalDate(String name) {
+        requireNotReserved(name);
         return new Property(name, true, ValueType.DATE);
     }
 
     public static Property internalDateBag(String name) {
+        requireNotReserved(name);
         return new Property(name, true, PropertyType.BAG, ValueType.DATE);
     }
 
     public static Property internalInteger(String name) {
+        requireNotReserved(name);
         return new Property(name, true, ValueType.INTEGER);
     }
 
     public static Property internalIntegerSequence(String name) {
+        requireNotReserved(name);
         return new Property(name, true, PropertyType.SEQ, ValueType.INTEGER);
     }
 
     public static Property internalRational(String name) {
+        requireNotReserved(name);
         return new Property(name, true, ValueType.RATIONAL);
     }
 
     public static Property internalOpenChoise(String name, String... choices) {
+        requireNotReserved(name);
         return new Property(name, true, ValueType.OPEN_CHOICE, choices);
     }
 
     public static Property internalReal(String name) {
+        requireNotReserved(name);
         return new Property(name, true, ValueType.REAL);
     }
 
     public static Property internalText(String name) {
+        requireNotReserved(name);
         return new Property(name, true, ValueType.TEXT);
     }
 
     public static Property internalTextBag(String name) {
+        requireNotReserved(name);
         return new Property(name, true, PropertyType.BAG, ValueType.TEXT);
     }
 
     public static Property internalURI(String name) {
+        requireNotReserved(name);
         return new Property(name, true, ValueType.URI);
     }
 
     public static Property externalClosedChoise(String name, String... 
choices) {
+        requireNotReserved(name);
         return new Property(name, false, ValueType.CLOSED_CHOICE, choices);
     }
 
     public static Property externalOpenChoise(String name, String... choices) {
+        requireNotReserved(name);
         return new Property(name, false, ValueType.OPEN_CHOICE, choices);
     }
 
     public static Property externalDate(String name) {
+        requireNotReserved(name);
         return new Property(name, false, ValueType.DATE);
     }
 
     public static Property externalReal(String name) {
+        requireNotReserved(name);
         return new Property(name, false, ValueType.REAL);
     }
 
     public static Property externalRealSeq(String name) {
+        requireNotReserved(name);
         return new Property(name, false, PropertyType.SEQ, ValueType.REAL);
     }
 
     public static Property externalInteger(String name) {
+        requireNotReserved(name);
         return new Property(name, false, ValueType.INTEGER);
     }
 
     public static Property externalBoolean(String name) {
+        requireNotReserved(name);
         return new Property(name, false, ValueType.BOOLEAN);
     }
 
     public static Property externalBooleanSeq(String name) {
+        requireNotReserved(name);
         return new Property(name, false, PropertyType.SEQ, ValueType.BOOLEAN);
     }
 
     public static Property externalText(String name) {
+        requireNotReserved(name);
         return new Property(name, false, ValueType.TEXT);
     }
 
     public static Property externalTextBag(String name) {
+        requireNotReserved(name);
+        return new Property(name, false, PropertyType.BAG, ValueType.TEXT);
+    }
+
+    // ---- Package-private mirrors for curated tk:/X-TIKA: constants 
-------------------
+    // Same shapes as the public factories above, but (a) assert the name IS 
reserved
+    // (b) still register (curated constants must stay resolvable via 
Property.get /
+    // Metadata.reconstruct). Callers: TikaCoreProperties, TikaPagedText, 
Rendering — all
+    // in-package. Not for document-derived names; those mint unregistered (see
+    // mintUnregistered) via KeyPrefix (stage 3+).
+
+    static Property reservedInternalBoolean(String name) {
+        requireReserved(name);
+        return new Property(name, true, ValueType.BOOLEAN);
+    }
+
+    static Property reservedInternalClosedChoise(String name, String... 
choices) {
+        requireReserved(name);
+        return new Property(name, true, ValueType.CLOSED_CHOICE, choices);
+    }
+
+    static Property reservedInternalDate(String name) {
+        requireReserved(name);
+        return new Property(name, true, ValueType.DATE);
+    }
+
+    static Property reservedInternalDateBag(String name) {
+        requireReserved(name);
+        return new Property(name, true, PropertyType.BAG, ValueType.DATE);
+    }
+
+    static Property reservedInternalInteger(String name) {
+        requireReserved(name);
+        return new Property(name, true, ValueType.INTEGER);
+    }
+
+    static Property reservedInternalIntegerSequence(String name) {
+        requireReserved(name);
+        return new Property(name, true, PropertyType.SEQ, ValueType.INTEGER);
+    }
+
+    static Property reservedInternalRational(String name) {
+        requireReserved(name);
+        return new Property(name, true, ValueType.RATIONAL);
+    }
+
+    static Property reservedInternalOpenChoise(String name, String... choices) 
{
+        requireReserved(name);
+        return new Property(name, true, ValueType.OPEN_CHOICE, choices);
+    }
+
+    static Property reservedInternalReal(String name) {
+        requireReserved(name);
+        return new Property(name, true, ValueType.REAL);
+    }
+
+    static Property reservedInternalText(String name) {
+        requireReserved(name);
+        return new Property(name, true, ValueType.TEXT);
+    }
+
+    static Property reservedInternalTextBag(String name) {
+        requireReserved(name);
+        return new Property(name, true, PropertyType.BAG, ValueType.TEXT);
+    }
+
+    static Property reservedInternalURI(String name) {
+        requireReserved(name);
+        return new Property(name, true, ValueType.URI);
+    }
+
+    static Property reservedExternalClosedChoise(String name, String... 
choices) {
+        requireReserved(name);
+        return new Property(name, false, ValueType.CLOSED_CHOICE, choices);
+    }
+
+    static Property reservedExternalOpenChoise(String name, String... choices) 
{
+        requireReserved(name);
+        return new Property(name, false, ValueType.OPEN_CHOICE, choices);
+    }
+
+    static Property reservedExternalDate(String name) {
+        requireReserved(name);
+        return new Property(name, false, ValueType.DATE);
+    }
+
+    static Property reservedExternalReal(String name) {
+        requireReserved(name);
+        return new Property(name, false, ValueType.REAL);
+    }
+
+    static Property reservedExternalRealSeq(String name) {
+        requireReserved(name);
+        return new Property(name, false, PropertyType.SEQ, ValueType.REAL);
+    }
+
+    static Property reservedExternalInteger(String name) {
+        requireReserved(name);
+        return new Property(name, false, ValueType.INTEGER);
+    }
+
+    static Property reservedExternalBoolean(String name) {
+        requireReserved(name);
+        return new Property(name, false, ValueType.BOOLEAN);
+    }
+
+    static Property reservedExternalBooleanSeq(String name) {
+        requireReserved(name);
+        return new Property(name, false, PropertyType.SEQ, ValueType.BOOLEAN);
+    }
+
+    static Property reservedExternalText(String name) {
+        requireReserved(name);
+        return new Property(name, false, ValueType.TEXT);
+    }
+
+    static Property reservedExternalTextBag(String name) {
+        requireReserved(name);
         return new Property(name, false, PropertyType.BAG, ValueType.TEXT);
     }
 
@@ -267,6 +434,10 @@ public final class Property implements 
Comparable<Property> {
      * <p>
      * Note that name of the composite property is taken from its primary 
property,
      * and primary and secondary properties must not be composite properties 
themselves.
+     * <p>
+     * No reserved-name check here: {@code primaryProperty} was already 
validated (or
+     * asserted reserved) at its own mint, and composites never register (see 
the
+     * constructor), so there is nothing new to forge or intern.
      *
      * @param primaryProperty
      * @param secondaryExtractProperties
diff --git a/tika-core/src/main/java/org/apache/tika/metadata/Rendering.java 
b/tika-core/src/main/java/org/apache/tika/metadata/Rendering.java
index 037dbb6a45..4eab31a2ac 100644
--- a/tika-core/src/main/java/org/apache/tika/metadata/Rendering.java
+++ b/tika-core/src/main/java/org/apache/tika/metadata/Rendering.java
@@ -19,6 +19,20 @@ package org.apache.tika.metadata;
 public interface Rendering {
     String RENDERING_PREFIX = "tk:rendering:";
 
-    Property RENDERED_BY = Property.externalTextBag(RENDERING_PREFIX + 
"rendered-by");
-    Property RENDERED_MS = Property.externalReal(RENDERING_PREFIX + 
"rendering-time-ms");
+    Property RENDERED_BY = Property.reservedExternalTextBag(RENDERING_PREFIX + 
"rendered-by");
+    Property RENDERED_MS = Property.reservedExternalReal(RENDERING_PREFIX + 
"rendering-time-ms");
+
+    /**
+     * Time PDFBox took to render the page to a {@code BufferedImage}.
+     * @see org.apache.tika.renderer.pdf.pdfbox.PDFBoxRenderer
+     */
+    Property PDFBOX_RENDERING_TIME_MS =
+            Property.reservedExternalReal(RENDERING_PREFIX + 
"pdfbox-rendering-ms");
+
+    /**
+     * Time PDFBox/java took to write the rendered image out (encoding cost 
varies by format).
+     * @see org.apache.tika.renderer.pdf.pdfbox.PDFBoxRenderer
+     */
+    Property PDFBOX_IMAGE_WRITING_TIME_MS =
+            Property.reservedExternalReal(RENDERING_PREFIX + 
"pdfbox-image-writing-ms");
 }
diff --git 
a/tika-core/src/main/java/org/apache/tika/metadata/TikaCoreProperties.java 
b/tika-core/src/main/java/org/apache/tika/metadata/TikaCoreProperties.java
index a977d8c377..0b63adc982 100644
--- a/tika-core/src/main/java/org/apache/tika/metadata/TikaCoreProperties.java
+++ b/tika-core/src/main/java/org/apache/tika/metadata/TikaCoreProperties.java
@@ -16,6 +16,8 @@
  */
 package org.apache.tika.metadata;
 
+import org.apache.tika.utils.StringUtils;
+
 /**
  * Contains a core set of basic Tika metadata properties, which all parsers
  * will attempt to supply (where the file format permits). These are all
@@ -58,7 +60,31 @@ public interface TikaCoreProperties {
      * migrate to {@link #TIKA_META_PREFIX} ({@code tk:}) — see the 3.x-to-4.x 
metadata key table.
      */
     String LEGACY_TIKA_META_PREFIX = "X-TIKA" + NAMESPACE_PREFIX_DELIMITER;
-    Property EMBEDDED_DEPTH = Property.internalInteger(TIKA_META_PREFIX + 
"embedded-depth");
+
+    /**
+     * Template factory for digest keys: {@code tk:digest:<suffix>}, e.g.
+     * {@code tk:digest:SHA256} or {@code tk:digest:SHA256:BASE32} 
(algorithm[:encoding],
+     * config-supplied by a {@code Digester}, never document-derived). 
Validates only that
+     * {@code suffix} is non-empty and contains no whitespace — the shape 
constraint is
+     * hygiene, not security, since the input is trusted config.
+     *
+     * @since Apache Tika 4.0.0
+     */
+    static Property digestProperty(String suffix) {
+        if (StringUtils.isBlank(suffix)) {
+            throw new IllegalArgumentException("digest suffix must not be 
blank");
+        }
+        for (int i = 0; i < suffix.length(); i++) {
+            if (Character.isWhitespace(suffix.charAt(i))) {
+                throw new IllegalArgumentException(
+                        "digest suffix must not contain whitespace: '" + 
suffix + "'");
+            }
+        }
+        return Property.reservedInternalText(
+                TIKA_META_PREFIX + "digest" + NAMESPACE_PREFIX_DELIMITER + 
suffix);
+    }
+
+    Property EMBEDDED_DEPTH = 
Property.reservedInternalInteger(TIKA_META_PREFIX + "embedded-depth");
 
     /**
      * This tracks the embedded file paths based on the name of embedded files
@@ -71,7 +97,7 @@ public interface TikaCoreProperties {
      * For a more robust path, see {@link TikaCoreProperties#EMBEDDED_ID_PATH}.
      */
     Property EMBEDDED_RESOURCE_PATH =
-            Property.internalText(TIKA_META_PREFIX + "embedded-resource-path");
+            Property.reservedInternalText(TIKA_META_PREFIX + 
"embedded-resource-path");
 
 
     /**
@@ -94,28 +120,28 @@ public interface TikaCoreProperties {
      * For a more robust path, see {@link TikaCoreProperties#EMBEDDED_ID_PATH}.
      */
     Property FINAL_EMBEDDED_RESOURCE_PATH =
-            Property.internalText(TIKA_META_PREFIX + 
"final-embedded-resource-path");
+            Property.reservedInternalText(TIKA_META_PREFIX + 
"final-embedded-resource-path");
 
     /**
      * This tracks the embedded file paths based on the embedded file's
      * {@link TikaCoreProperties#EMBEDDED_ID}.
      */
     Property EMBEDDED_ID_PATH =
-            Property.internalText(TIKA_META_PREFIX + "embedded-id-path");
+            Property.reservedInternalText(TIKA_META_PREFIX + 
"embedded-id-path");
 
     /**
      * This is a 1-index counter for embedded files, used by the 
RecursiveParserWrapper
      */
     Property EMBEDDED_ID =
-            Property.internalInteger(TIKA_META_PREFIX + "embedded-id");
+            Property.reservedInternalInteger(TIKA_META_PREFIX + "embedded-id");
 
-    Property PARSE_TIME_MILLIS = Property.internalText(TIKA_META_PREFIX + 
"parse-time-millis");
+    Property PARSE_TIME_MILLIS = 
Property.reservedInternalText(TIKA_META_PREFIX + "parse-time-millis");
     /**
      * Simple class name of the content handler.
      * @deprecated Use {@link #TIKA_CONTENT_HANDLER_TYPE} for the handler type 
enum value.
      */
     @Deprecated
-    Property TIKA_CONTENT_HANDLER = Property.internalText(TIKA_META_PREFIX + 
"content-handler");
+    Property TIKA_CONTENT_HANDLER = 
Property.reservedInternalText(TIKA_META_PREFIX + "content-handler");
 
     /**
      * The handler type used to produce {@link #TIKA_CONTENT}.
@@ -123,15 +149,15 @@ public interface TikaCoreProperties {
      * enum name (e.g. {@code TEXT}, {@code MARKDOWN}, {@code HTML}, {@code 
XML}).
      */
     Property TIKA_CONTENT_HANDLER_TYPE =
-            Property.internalText(TIKA_META_PREFIX + "content-handler-type");
-    Property TIKA_CONTENT = Property.internalText(TIKA_META_PREFIX + 
"content");
+            Property.reservedInternalText(TIKA_META_PREFIX + 
"content-handler-type");
+    Property TIKA_CONTENT = Property.reservedInternalText(TIKA_META_PREFIX + 
"content");
 
     /**
      * JSON array of chunks (text segments with optional embedding vectors and 
locators).
      * Used by inference parsers and metadata filters to attach chunked 
representations
      * of document content for downstream indexing and semantic search.
      */
-    Property TIKA_CHUNKS = Property.internalText("tk:chunks");
+    Property TIKA_CHUNKS = Property.reservedInternalText("tk:chunks");
     /**
      * Use this to store parse exception information in the Metadata object.
      */
@@ -144,28 +170,36 @@ public interface TikaCoreProperties {
 
     //exception in main file
     Property CONTAINER_EXCEPTION =
-            Property.internalText(TIKA_META_EXCEPTION_PREFIX + 
"container-exception");
+            Property.reservedInternalText(TIKA_META_EXCEPTION_PREFIX + 
"container-exception");
 
     //exception in an embedded file
     Property EMBEDDED_EXCEPTION =
-            Property.internalTextBag(TIKA_META_EXCEPTION_PREFIX + 
"embedded-exception");
+            Property.reservedInternalTextBag(TIKA_META_EXCEPTION_PREFIX + 
"embedded-exception");
 
     //exception handling the raw bytes of an embedded file by an 
EmbeddedDocumentByteStore
     Property EMBEDDED_BYTES_EXCEPTION =
-            Property.internalTextBag(TIKA_META_EXCEPTION_PREFIX + 
"embedded-bytes-exception");
+            Property.reservedInternalTextBag(TIKA_META_EXCEPTION_PREFIX + 
"embedded-bytes-exception");
 
     //warning while parsing in an embedded file
     Property EMBEDDED_WARNING =
-            Property.internalTextBag(TIKA_META_EXCEPTION_PREFIX + 
"embedded-warning");
+            Property.reservedInternalTextBag(TIKA_META_EXCEPTION_PREFIX + 
"embedded-warning");
+
+    /**
+     * Identifies the embedded {@link org.apache.tika.parser.Parser} that 
produced
+     * {@link #EMBEDDED_EXCEPTION}, when known.
+     * @see org.apache.tika.utils.ParserUtils#EMBEDDED_PARSER the alias parser 
code uses
+     */
+    Property EMBEDDED_PARSER =
+            Property.reservedInternalText(TIKA_META_EXCEPTION_PREFIX + 
"embedded-parser");
 
     Property WRITE_LIMIT_REACHED =
-            Property.internalBoolean(TIKA_META_EXCEPTION_PREFIX + 
"write-limit-reached");
+            Property.reservedInternalBoolean(TIKA_META_EXCEPTION_PREFIX + 
"write-limit-reached");
 
     Property EMBEDDED_RESOURCE_LIMIT_REACHED =
-            Property.internalBoolean(TIKA_META_EXCEPTION_PREFIX + 
"embedded-resource-limit-reached");
+            Property.reservedInternalBoolean(TIKA_META_EXCEPTION_PREFIX + 
"embedded-resource-limit-reached");
 
     Property EMBEDDED_DEPTH_LIMIT_REACHED =
-            Property.internalBoolean(TIKA_META_EXCEPTION_PREFIX + 
"embedded-depth-limit-reached");
+            Property.reservedInternalBoolean(TIKA_META_EXCEPTION_PREFIX + 
"embedded-depth-limit-reached");
 
     /**
      * Use this to store exceptions caught during a parse that are
@@ -174,7 +208,7 @@ public interface TikaCoreProperties {
      * a dependency.
      */
     Property TIKA_META_EXCEPTION_WARNING =
-            Property.internalTextBag(TIKA_META_EXCEPTION_PREFIX + "warn");
+            Property.reservedInternalTextBag(TIKA_META_EXCEPTION_PREFIX + 
"warn");
 
     /**
      * This means that metadata keys or metadata values were truncated.
@@ -182,7 +216,7 @@ public interface TikaCoreProperties {
      * a field is not in the "include" set.
      */
     Property TRUNCATED_METADATA =
-            Property.internalBoolean(TIKA_META_WARN_PREFIX + 
"truncated-metadata");
+            Property.reservedInternalBoolean(TIKA_META_WARN_PREFIX + 
"truncated-metadata");
 
     /**
      * This indicates that only a portion of the file content was provided for 
detection.
@@ -190,7 +224,7 @@ public interface TikaCoreProperties {
      * (e.g., not returning a detection result that requires reading to end of 
file).
      */
     Property TRUNCATED_CONTENT_FOR_DETECTION =
-            Property.internalBoolean(TIKA_META_PREFIX + 
"truncated-content-for-detection");
+            Property.reservedInternalBoolean(TIKA_META_PREFIX + 
"truncated-content-for-detection");
 
     /**
      * When content is truncated for detection, this stores the number of bytes
@@ -198,7 +232,7 @@ public interface TikaCoreProperties {
      * to set appropriate mark limits.
      */
     Property DETECTION_CONTENT_LENGTH =
-            Property.internalInteger(TIKA_META_PREFIX + 
"detection-content-length");
+            Property.reservedInternalInteger(TIKA_META_PREFIX + 
"detection-content-length");
 
     /**
      * Use this to store exceptions caught while trying to read the
@@ -206,34 +240,34 @@ public interface TikaCoreProperties {
      * a parse exception on the embedded resource.
      */
     Property TIKA_META_EXCEPTION_EMBEDDED_STREAM =
-            Property.internalTextBag(TIKA_META_EXCEPTION_PREFIX + 
"embedded-stream-exception");
-    Property TIKA_PARSED_BY = Property.internalTextBag(TIKA_META_PREFIX + 
"parsed-by");
+            Property.reservedInternalTextBag(TIKA_META_EXCEPTION_PREFIX + 
"embedded-stream-exception");
+    Property TIKA_PARSED_BY = 
Property.reservedInternalTextBag(TIKA_META_PREFIX + "parsed-by");
 
     /**
      * Use this to store a record of all parsers that touched a given file
      * in the container file's metadata.
      */
-    Property TIKA_PARSED_BY_FULL_SET = 
Property.internalTextBag(TIKA_META_PREFIX + "parsed-by-full-set");
+    Property TIKA_PARSED_BY_FULL_SET = 
Property.reservedInternalTextBag(TIKA_META_PREFIX + "parsed-by-full-set");
 
-    Property TIKA_DETECTED_LANGUAGE = 
Property.externalTextBag(TIKA_META_PREFIX +
+    Property TIKA_DETECTED_LANGUAGE = 
Property.reservedExternalTextBag(TIKA_META_PREFIX +
             "detected-language");
 
-    Property TIKA_DETECTED_LANGUAGE_CONFIDENCE = 
Property.externalTextBag(TIKA_META_PREFIX +
+    Property TIKA_DETECTED_LANGUAGE_CONFIDENCE = 
Property.reservedExternalTextBag(TIKA_META_PREFIX +
             "detected-language-confidence");
 
-    Property TIKA_DETECTED_LANGUAGE_CONFIDENCE_RAW = 
Property.externalRealSeq(TIKA_META_PREFIX +
+    Property TIKA_DETECTED_LANGUAGE_CONFIDENCE_RAW = 
Property.reservedExternalRealSeq(TIKA_META_PREFIX +
             "detected-language-confidence-raw");
 
-    Property RESOURCE_NAME_KEY = Property.internalText(TIKA_META_PREFIX + 
"resource-name");
+    Property RESOURCE_NAME_KEY = 
Property.reservedInternalText(TIKA_META_PREFIX + "resource-name");
 
     /**
      * Indicates that the file extension on the resource name was inferred by 
Tika
      * (e.g., from content type detection) rather than provided by the 
original document.
      */
     Property RESOURCE_NAME_EXTENSION_INFERRED =
-            Property.externalBoolean(TIKA_META_PREFIX + 
"resource-name-extension-inferred");
+            Property.reservedExternalBoolean(TIKA_META_PREFIX + 
"resource-name-extension-inferred");
 
-    Property EMBEDDED_RELATIONSHIP_ID = Property.internalText(TIKA_META_PREFIX 
+ "embedded-relationship-id");
+    Property EMBEDDED_RELATIONSHIP_ID = 
Property.reservedInternalText(TIKA_META_PREFIX + "embedded-relationship-id");
 
     String EMBEDDED_RESOURCE_TYPE_KEY = "tk:embedded-resource-type";
     /**
@@ -242,7 +276,7 @@ public interface TikaCoreProperties {
      * within the file.
      */
     Property ORIGINAL_RESOURCE_NAME =
-            Property.internalTextBag(TIKA_META_PREFIX + "orig-resource-name");
+            Property.reservedInternalTextBag(TIKA_META_PREFIX + 
"orig-resource-name");
     /**
      * This should be used to store the path (relative or full)
      * of the source/container file, including the file name,
@@ -250,13 +284,13 @@ public interface TikaCoreProperties {
      * <p>
      * This can also be used for a primary key within a database.
      */
-    Property SOURCE_PATH = Property.internalText(TIKA_META_PREFIX + 
"source-path");
+    Property SOURCE_PATH = Property.reservedInternalText(TIKA_META_PREFIX + 
"source-path");
 
     /**
      * This records the metadata as stored within a file for an embedded 
file's path
      * including the file name. For example a zip file may include an msg with 
this path: /my-emails/important/this.msg
      */
-    Property INTERNAL_PATH = Property.internalText(TIKA_META_PREFIX + 
"internal-path");
+    Property INTERNAL_PATH = Property.reservedInternalText(TIKA_META_PREFIX + 
"internal-path");
 
     /**
      * This is currently used to identify Content-Type that may be
@@ -265,25 +299,25 @@ public interface TikaCoreProperties {
      * , or the value might come from outside the document.  This information
      * may be faulty and should be treated only as a hint.
      */
-    Property CONTENT_TYPE_HINT = Property.internalText(TIKA_META_PREFIX + 
"content-type-hint");
+    Property CONTENT_TYPE_HINT = 
Property.reservedInternalText(TIKA_META_PREFIX + "content-type-hint");
     /**
      * This is used by users to override detection with the override detector.
      */
     Property CONTENT_TYPE_USER_OVERRIDE =
-            Property.internalText(TIKA_META_PREFIX + "content-type-override");
+            Property.reservedInternalText(TIKA_META_PREFIX + 
"content-type-override");
     /**
      * This is used by parsers to override detection of embedded resources
      * with the override detector.
      */
     Property CONTENT_TYPE_PARSER_OVERRIDE =
-            Property.internalText(TIKA_META_PREFIX + 
"content-type-parser-override");
+            Property.reservedInternalText(TIKA_META_PREFIX + 
"content-type-parser-override");
     /**
      * This is set by DefaultDetector to store the result of MimeTypes (magic 
byte)
      * detection. This allows downstream detectors to use it as a hint without
      * re-running magic detection.
      */
     Property CONTENT_TYPE_MAGIC_DETECTED =
-            Property.internalText(TIKA_META_PREFIX + 
"content-type-magic-detected");
+            Property.reservedInternalText(TIKA_META_PREFIX + 
"content-type-magic-detected");
     /**
      * @see DublinCore#FORMAT
      */
@@ -396,7 +430,7 @@ public interface TikaCoreProperties {
      * Java's {@link javax.imageio.ImageReader#getNumImages(boolean)}.  See
      * the javadocs for known limitations.
      */
-    Property NUM_IMAGES = Property.internalInteger("tk:num-images");
+    Property NUM_IMAGES = Property.reservedInternalInteger("tk:num-images");
 
     // Comment and rating properties
     /**
@@ -406,21 +440,21 @@ public interface TikaCoreProperties {
     /**
      * Embedded resource type property
      */
-    Property EMBEDDED_RESOURCE_TYPE = 
Property.internalClosedChoise(EMBEDDED_RESOURCE_TYPE_KEY,
+    Property EMBEDDED_RESOURCE_TYPE = 
Property.reservedInternalClosedChoise(EMBEDDED_RESOURCE_TYPE_KEY,
             EmbeddedResourceType.ATTACHMENT.toString(), 
EmbeddedResourceType.INLINE.toString(),
             EmbeddedResourceType.METADATA.toString(), 
EmbeddedResourceType.MACRO.toString(),
             EmbeddedResourceType.THUMBNAIL.toString(), 
EmbeddedResourceType.RENDERING.toString());
-    Property HAS_SIGNATURE = Property.internalBoolean("tk:has-signature");
+    Property HAS_SIGNATURE = 
Property.reservedInternalBoolean("tk:has-signature");
 
-    Property SIGNATURE_NAME = Property.internalTextBag("tk:signature:name");
-    Property SIGNATURE_DATE = Property.internalDateBag("tk:signature:date");
-    Property SIGNATURE_LOCATION = 
Property.internalTextBag("tk:signature:location");
-    Property SIGNATURE_REASON = 
Property.internalTextBag("tk:signature:reason");
-    Property SIGNATURE_FILTER = 
Property.internalTextBag("tk:signature:filter");
-    Property SIGNATURE_CONTACT_INFO = 
Property.internalTextBag("tk:signature:contact-info");
+    Property SIGNATURE_NAME = 
Property.reservedInternalTextBag("tk:signature:name");
+    Property SIGNATURE_DATE = 
Property.reservedInternalDateBag("tk:signature:date");
+    Property SIGNATURE_LOCATION = 
Property.reservedInternalTextBag("tk:signature:location");
+    Property SIGNATURE_REASON = 
Property.reservedInternalTextBag("tk:signature:reason");
+    Property SIGNATURE_FILTER = 
Property.reservedInternalTextBag("tk:signature:filter");
+    Property SIGNATURE_CONTACT_INFO = 
Property.reservedInternalTextBag("tk:signature:contact-info");
 
     //is the file encrypted
-    Property IS_ENCRYPTED = Property.internalBoolean(TIKA_META_PREFIX + 
"encrypted");
+    Property IS_ENCRYPTED = Property.reservedInternalBoolean(TIKA_META_PREFIX 
+ "encrypted");
 
     /**
      * When an EncodingDetector detects an encoding, the encoding should be 
stored in this field.
@@ -429,14 +463,14 @@ public interface TikaCoreProperties {
      * may choose to use a default encoding. We want to differentiate between 
a parser using a
      * default encoding and the output of an EncodingDetector.
      */
-    Property DETECTED_ENCODING = Property.externalText(TIKA_META_PREFIX + 
"detected-encoding");
+    Property DETECTED_ENCODING = 
Property.reservedExternalText(TIKA_META_PREFIX + "detected-encoding");
 
 
     /**
      * This should be the simple class name for the EncodingDetectors whose 
detected encoding
      * was used in the parse.
      */
-    Property ENCODING_DETECTOR = Property.externalText(TIKA_META_PREFIX + 
"encoding-detector");
+    Property ENCODING_DETECTOR = 
Property.reservedExternalText(TIKA_META_PREFIX + "encoding-detector");
 
     /**
      * Diagnostic trace showing which encoding detectors ran and what each 
returned,
@@ -444,7 +478,7 @@ public interface TikaCoreProperties {
      * Example: {@code "HtmlEncodingDetector->UTF-8, 
Icu4jEncodingDetector->windows-1256 (scored)"}
      */
     Property ENCODING_DETECTION_TRACE =
-            Property.externalText(TIKA_META_PREFIX + 
"encoding-detection-trace");
+            Property.reservedExternalText(TIKA_META_PREFIX + 
"encoding-detection-trace");
 
     /**
      * The charset actually used to decode the stream when a superset override 
was applied.
@@ -456,13 +490,13 @@ public interface TikaCoreProperties {
      * the same charset.
      */
     Property DECODED_CHARSET =
-            Property.externalText(TIKA_META_PREFIX + "decoded-charset");
+            Property.reservedExternalText(TIKA_META_PREFIX + 
"decoded-charset");
 
     /**
      * General metadata key for the count of non-final versions available 
within a file.  This
      * was added initially to support generalizing incremental updates in PDF.
      */
-    Property VERSION_COUNT = Property.externalInteger(TIKA_META_PREFIX + 
"version-count");
+    Property VERSION_COUNT = Property.reservedExternalInteger(TIKA_META_PREFIX 
+ "version-count");
 
     /**
      * General metadata key for the version number of a given file that 
contains
@@ -470,9 +504,9 @@ public interface TikaCoreProperties {
      * The latest version does not have this metadata value.  This was added 
initially
      * to support generalizing incremental updates in PDF.
      */
-    Property VERSION_NUMBER = Property.externalInteger(TIKA_META_PREFIX + 
"version-number");
+    Property VERSION_NUMBER = 
Property.reservedExternalInteger(TIKA_META_PREFIX + "version-number");
 
-    Property PIPES_RESULT = Property.externalText(TIKA_META_PREFIX + 
"pipes-result");
+    Property PIPES_RESULT = Property.reservedExternalText(TIKA_META_PREFIX + 
"pipes-result");
     /**
      * A file might contain different types of embedded documents.
      * The most common is the ATTACHMENT.
diff --git 
a/tika-core/src/main/java/org/apache/tika/metadata/TikaPagedText.java 
b/tika-core/src/main/java/org/apache/tika/metadata/TikaPagedText.java
index 76faee52bd..0251e01fd0 100644
--- a/tika-core/src/main/java/org/apache/tika/metadata/TikaPagedText.java
+++ b/tika-core/src/main/java/org/apache/tika/metadata/TikaPagedText.java
@@ -30,7 +30,7 @@ public interface TikaPagedText {
      * anchored to exactly one page; for resources spanning multiple pages
      * see {@link #PAGE_NUMBERS}.
      */
-    Property PAGE_NUMBER = Property.internalInteger(TIKA_PAGED_TEXT_PREFIX + 
"number");
+    Property PAGE_NUMBER = 
Property.reservedInternalInteger(TIKA_PAGED_TEXT_PREFIX + "number");
 
     /**
      * 1-based page numbers an embedded resource is anchored to, as a
@@ -56,7 +56,7 @@ public interface TikaPagedText {
      * </ul>
      */
     Property PAGE_NUMBERS =
-            Property.internalIntegerSequence(TIKA_PAGED_TEXT_PREFIX + 
"numbers");
+            Property.reservedInternalIntegerSequence(TIKA_PAGED_TEXT_PREFIX + 
"numbers");
 
     /**
      * Sentinel value used as the sole element of {@link #PAGE_NUMBERS}
@@ -66,5 +66,5 @@ public interface TikaPagedText {
      */
     int UNLINKED_PAGE = -1;
 
-    Property PAGE_ROTATION = Property.internalRational(TIKA_PAGED_TEXT_PREFIX 
+ "rotation");
+    Property PAGE_ROTATION = 
Property.reservedInternalRational(TIKA_PAGED_TEXT_PREFIX + "rotation");
 }
diff --git a/tika-core/src/main/java/org/apache/tika/utils/ParserUtils.java 
b/tika-core/src/main/java/org/apache/tika/utils/ParserUtils.java
index 3536cf83e6..e194536076 100644
--- a/tika-core/src/main/java/org/apache/tika/utils/ParserUtils.java
+++ b/tika-core/src/main/java/org/apache/tika/utils/ParserUtils.java
@@ -31,8 +31,11 @@ import org.apache.tika.parser.ParserDecorator;
  */
 public class ParserUtils {
 
-    public final static Property EMBEDDED_PARSER = Property.internalText(
-            TikaCoreProperties.TIKA_META_EXCEPTION_PREFIX + "embedded-parser");
+    /**
+     * @see TikaCoreProperties#EMBEDDED_PARSER the curated Property this 
aliases; {@code tk:}
+     * Properties mint only in org.apache.tika.metadata (see Property 
mint-time validation)
+     */
+    public final static Property EMBEDDED_PARSER = 
TikaCoreProperties.EMBEDDED_PARSER;
 
 
     /**
diff --git 
a/tika-core/src/test/java/org/apache/tika/metadata/PropertyReservedNameTest.java
 
b/tika-core/src/test/java/org/apache/tika/metadata/PropertyReservedNameTest.java
new file mode 100644
index 0000000000..dc1708f23b
--- /dev/null
+++ 
b/tika-core/src/test/java/org/apache/tika/metadata/PropertyReservedNameTest.java
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.metadata;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Stage 2 mint-time validation: the public {@code Property} factories reject 
reserved
+ * ({@code tk:}/{@code X-TIKA:}) names, curated reserved constants mint 
through the
+ * package-private path instead, and the digest template factory produces 
usable Properties.
+ */
+public class PropertyReservedNameTest {
+
+    // Every public static factory that mints a (non-composite) Property, 
keyed by method
+    // name for readable failure messages. Table-driven so one test method 
covers all 22
+    // shapes instead of 22 near-identical methods (no junit-jupiter-params in 
this module).
+    private static final Map<String, Function<String, Property>> 
PUBLIC_FACTORIES = publicFactories();
+
+    private static Map<String, Function<String, Property>> publicFactories() {
+        Map<String, Function<String, Property>> m = new LinkedHashMap<>();
+        m.put("internalBoolean", Property::internalBoolean);
+        m.put("internalClosedChoise", n -> Property.internalClosedChoise(n, 
"a", "b"));
+        m.put("internalDate", Property::internalDate);
+        m.put("internalDateBag", Property::internalDateBag);
+        m.put("internalInteger", Property::internalInteger);
+        m.put("internalIntegerSequence", Property::internalIntegerSequence);
+        m.put("internalRational", Property::internalRational);
+        m.put("internalOpenChoise", n -> Property.internalOpenChoise(n, "a", 
"b"));
+        m.put("internalReal", Property::internalReal);
+        m.put("internalText", Property::internalText);
+        m.put("internalTextBag", Property::internalTextBag);
+        m.put("internalURI", Property::internalURI);
+        m.put("externalClosedChoise", n -> Property.externalClosedChoise(n, 
"a", "b"));
+        m.put("externalOpenChoise", n -> Property.externalOpenChoise(n, "a", 
"b"));
+        m.put("externalDate", Property::externalDate);
+        m.put("externalReal", Property::externalReal);
+        m.put("externalRealSeq", Property::externalRealSeq);
+        m.put("externalInteger", Property::externalInteger);
+        m.put("externalBoolean", Property::externalBoolean);
+        m.put("externalBooleanSeq", Property::externalBooleanSeq);
+        m.put("externalText", Property::externalText);
+        m.put("externalTextBag", Property::externalTextBag);
+        return m;
+    }
+
+    @Test
+    public void testAllPublicFactoriesRejectTkPrefix() {
+        for (Map.Entry<String, Function<String, Property>> e : 
PUBLIC_FACTORIES.entrySet()) {
+            String label = e.getKey();
+            Function<String, Property> factory = e.getValue();
+            String name = "tk:prop-test-reserved-" + label + "-" + 
System.nanoTime();
+            assertThrows(IllegalArgumentException.class, () -> 
factory.apply(name),
+                    label + " must reject a tk: name");
+        }
+    }
+
+    @Test
+    public void testAllPublicFactoriesRejectLegacyXTikaPrefix() {
+        for (Map.Entry<String, Function<String, Property>> e : 
PUBLIC_FACTORIES.entrySet()) {
+            String label = e.getKey();
+            Function<String, Property> factory = e.getValue();
+            String name = "X-TIKA:prop-test-reserved-" + label + "-" + 
System.nanoTime();
+            assertThrows(IllegalArgumentException.class, () -> 
factory.apply(name),
+                    label + " must reject an X-TIKA: name");
+        }
+    }
+
+    @Test
+    public void testAllPublicFactoriesStillWorkForNonReservedNames() {
+        for (Map.Entry<String, Function<String, Property>> e : 
PUBLIC_FACTORIES.entrySet()) {
+            String label = e.getKey();
+            String name = "prop-test:reserved-ok-" + label + "-" + 
System.nanoTime();
+            Property p = e.getValue().apply(name);
+            assertEquals(name, p.getName(), label + " should still mint a 
non-reserved name");
+            assertSame(p, Property.get(name), label + " should still 
register");
+        }
+    }
+
+    @Test
+    public void testReservedFactoryRegistersAndResolvesViaPropertyGet() {
+        String name = "tk:prop-test-curated-" + System.nanoTime();
+        Property p = Property.reservedInternalText(name);
+        assertSame(p, Property.get(name), "curated tk: constants must stay in 
the global registry");
+    }
+
+    @Test
+    public void testReservedFactoryRejectsNonReservedName() {
+        // symmetry check: the reserved path must not silently accept a 
non-reserved name
+        String name = "prop-test:not-reserved-" + System.nanoTime();
+        assertThrows(IllegalArgumentException.class, () -> 
Property.reservedInternalText(name));
+    }
+
+    @Test
+    public void testDigestPropertyProducesUsableRegisteredProperty() {
+        String suffix = "SHA256-" + System.nanoTime();
+        Property p = TikaCoreProperties.digestProperty(suffix);
+
+        assertEquals("tk:digest:" + suffix, p.getName());
+        assertSame(p, Property.get(p.getName()), "digest Properties must 
register");
+
+        Metadata metadata = new Metadata();
+        metadata.set(p, "abc123");
+        assertEquals("abc123", metadata.get(p.getName()));
+    }
+
+    @Test
+    public void testDigestPropertyRejectsBlankOrNullSuffix() {
+        assertThrows(IllegalArgumentException.class, () -> 
TikaCoreProperties.digestProperty(""));
+        assertThrows(IllegalArgumentException.class, () -> 
TikaCoreProperties.digestProperty(null));
+        assertThrows(IllegalArgumentException.class, () -> 
TikaCoreProperties.digestProperty("   "));
+    }
+
+    @Test
+    public void testDigestPropertyRejectsWhitespaceInSuffix() {
+        assertThrows(IllegalArgumentException.class,
+                () -> TikaCoreProperties.digestProperty("SHA 256"));
+    }
+
+    @Test
+    public void testDigestPropertyAllowsColonSeparatedEncodingSuffix() {
+        // real shape produced by DigestDef.metadataKey(), e.g. "SHA256:BASE32"
+        String suffix = "SHA256:BASE32-" + System.nanoTime();
+        Property p = TikaCoreProperties.digestProperty(suffix);
+        assertEquals("tk:digest:" + suffix, p.getName());
+    }
+
+    @Test
+    public void testMintUnregisteredPathUnaffectedByReservedNameChecks() {
+        // mintUnregistered carries no reserved-name validation either way -- 
it is the
+        // non-registering path used by curated in-package code today and by 
KeyPrefix from
+        // stage 3; this documents that stage 2 left it untouched.
+        String name = "tk:prop-test-unregistered-" + System.nanoTime();
+        Property p = Property.mintUnregistered(name, true, 
Property.PropertyType.SIMPLE,
+                Property.ValueType.TEXT, null);
+        assertEquals(name, p.getName());
+        assertNull(Property.get(name), "mintUnregistered must never intern");
+    }
+}
diff --git 
a/tika-eval/tika-eval-app/src/main/java/org/apache/tika/eval/app/ProfilerBase.java
 
b/tika-eval/tika-eval-app/src/main/java/org/apache/tika/eval/app/ProfilerBase.java
index 330455e610..8a26269383 100644
--- 
a/tika-eval/tika-eval-app/src/main/java/org/apache/tika/eval/app/ProfilerBase.java
+++ 
b/tika-eval/tika-eval-app/src/main/java/org/apache/tika/eval/app/ProfilerBase.java
@@ -71,7 +71,6 @@ import org.apache.tika.language.detect.LanguageResult;
 import org.apache.tika.metadata.Metadata;
 import org.apache.tika.metadata.PDF;
 import org.apache.tika.metadata.PagedText;
-import org.apache.tika.metadata.Property;
 import org.apache.tika.metadata.TikaCoreProperties;
 import org.apache.tika.ml.junkdetect.JunkDetector;
 import org.apache.tika.pipes.api.fetcher.FetchKey;
@@ -86,8 +85,10 @@ public abstract class ProfilerBase {
     protected static final AtomicInteger ID = new AtomicInteger();
     static final long NON_EXISTENT_FILE_LENGTH = -1l;
     final static int FILE_PATH_MAX_LEN = 1024;//max len for varchar for 
file_path
-    //Container exception key from the 1.x branch
-    private static final Property CONTAINER_EXCEPTION_1X = 
Property.externalText("X-TIKA" + ":EXCEPTION:runtime");
+    //Container exception key from the 1.x branch. Read-only lookup against 
legacy extract
+    //JSON, so a plain String key suffices -- Metadata.get(Property) is just 
get(name) anyway,
+    //and X-TIKA: is reserved: only org.apache.tika.metadata may mint a 
Property for it.
+    private static final String CONTAINER_EXCEPTION_1X = "X-TIKA" + 
":EXCEPTION:runtime";
     private static final Logger LOG = 
LoggerFactory.getLogger(ProfilerBase.class);
     private static final String[] EXTRACT_EXTENSIONS = {".json", ".txt", ""};
     private static final String[] COMPRESSION_EXTENSIONS = {"", ".bz2", 
".gzip", ".zip",};
diff --git 
a/tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-key-fields.json
 
b/tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-key-fields.json
index bba36567e8..94488c9af7 100644
--- 
a/tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-key-fields.json
+++ 
b/tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-key-fields.json
@@ -387,6 +387,8 @@
   
{"class":"org.apache.tika.metadata.RTFMetadata","field":"EMBEDDED_ITEM","key":"rtf:embedded-item"},
   
{"class":"org.apache.tika.metadata.RTFMetadata","field":"EMBEDDED_TOPIC","key":"rtf:embedded-topic"},
   
{"class":"org.apache.tika.metadata.RTFMetadata","field":"THUMBNAIL","key":"rtf:thumbnail"},
+  
{"class":"org.apache.tika.metadata.Rendering","field":"PDFBOX_IMAGE_WRITING_TIME_MS","key":"tk:rendering:pdfbox-image-writing-ms"},
+  
{"class":"org.apache.tika.metadata.Rendering","field":"PDFBOX_RENDERING_TIME_MS","key":"tk:rendering:pdfbox-rendering-ms"},
   
{"class":"org.apache.tika.metadata.Rendering","field":"RENDERED_BY","key":"tk:rendering:rendered-by"},
   
{"class":"org.apache.tika.metadata.Rendering","field":"RENDERED_MS","key":"tk:rendering:rendering-time-ms"},
   
{"class":"org.apache.tika.metadata.TIFF","field":"BITS_PER_SAMPLE","key":"tiff:BitsPerSample"},
@@ -433,6 +435,7 @@
   
{"class":"org.apache.tika.metadata.TikaCoreProperties","field":"EMBEDDED_EXCEPTION","key":"tk:exception:embedded-exception"},
   
{"class":"org.apache.tika.metadata.TikaCoreProperties","field":"EMBEDDED_ID","key":"tk:embedded-id"},
   
{"class":"org.apache.tika.metadata.TikaCoreProperties","field":"EMBEDDED_ID_PATH","key":"tk:embedded-id-path"},
+  
{"class":"org.apache.tika.metadata.TikaCoreProperties","field":"EMBEDDED_PARSER","key":"tk:exception:embedded-parser"},
   
{"class":"org.apache.tika.metadata.TikaCoreProperties","field":"EMBEDDED_RELATIONSHIP_ID","key":"tk:embedded-relationship-id"},
   
{"class":"org.apache.tika.metadata.TikaCoreProperties","field":"EMBEDDED_RESOURCE_LIMIT_REACHED","key":"tk:exception:embedded-resource-limit-reached"},
   
{"class":"org.apache.tika.metadata.TikaCoreProperties","field":"EMBEDDED_RESOURCE_PATH","key":"tk:embedded-resource-path"},
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/renderer/pdf/pdfbox/PDFBoxRenderer.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/renderer/pdf/pdfbox/PDFBoxRenderer.java
index fad55d468c..8c5c94592a 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/renderer/pdf/pdfbox/PDFBoxRenderer.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/renderer/pdf/pdfbox/PDFBoxRenderer.java
@@ -64,17 +64,21 @@ public class PDFBoxRenderer implements PDDocumentRenderer {
     /**
      * This is the amount of time it takes for PDFBox to render the page
      * to a BufferedImage
+     *
+     * @see Rendering#PDFBOX_RENDERING_TIME_MS the curated Property this 
aliases; {@code tk:}
+     * Properties mint only in org.apache.tika.metadata (see Property 
mint-time validation)
      */
-    public static Property PDFBOX_RENDERING_TIME_MS =
-            Property.externalReal(Rendering.RENDERING_PREFIX + 
"pdfbox-rendering-ms");
+    public static final Property PDFBOX_RENDERING_TIME_MS = 
Rendering.PDFBOX_RENDERING_TIME_MS;
 
     /**
      * This is the amount of time it takes for PDFBox/java to write the image 
after
      * it has been rendered into a BufferedImage.  Some formats take much 
longer
      * to encode than others.
+     *
+     * @see Rendering#PDFBOX_IMAGE_WRITING_TIME_MS the curated Property this 
aliases
      */
-    public static Property PDFBOX_IMAGE_WRITING_TIME_MS =
-            Property.externalReal(Rendering.RENDERING_PREFIX + 
"pdfbox-image-writing-ms");
+    public static final Property PDFBOX_IMAGE_WRITING_TIME_MS =
+            Rendering.PDFBOX_IMAGE_WRITING_TIME_MS;
 
     @Override
     public Set<MediaType> getSupportedTypes(ParseContext context) {

Reply via email to