KeyPrefix.java
/*
* 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 java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
* A Tika-owned prefix under which keys are passed through from the source: the prefix is fixed, but
* each key name comes verbatim from the document or tool, so keys are unbounded and can't be
* {@link Property} constants. Declaring one self-registers it, so the open set is enumerable (as
* {@link Property} makes the closed set) and lintable: a String write is legitimate iff its key is a
* registered {@link Property} or its prefix is a registered {@code KeyPrefix}.
*
* <p>Writes under a prefix go through {@link Metadata#add(KeyPrefix, String, String)} (or its
* {@code Instant} overload for source-typed dates) ��� append-only, with built-in skip-and-WARN
* handling of blank, over-length, or flooding source-derived names.</p>
*
* <p><strong>{@code KeyPrefix} instances are declaration-time constants.</strong> Declare one as a
* {@code static final} field, the same way a curated {@link Property} constant is declared ��� never
* construct one from document-derived text, and never construct one per-parse: the registry is
* static and unbounded, so per-parse construction grows it forever. An identical redeclaration
* (class re-init in another classloader) returns the incumbent; a conflicting one throws
* {@link IllegalStateException}.
*
* @since Apache Tika 4.0.0
*/
public final class KeyPrefix {
public enum Provenance { FILE, TOOL }
/** Delimiters accepted at the end of a prefix ��� the complete live population uses only these
* two ({@code geotopic:alt-}, {@code ogg:streams-}); '.' and '_' were retired with the
* envi./NER_ renames. Loosening later is free; tightening later is breaking. */
private static final char[] TRAILING_DELIMITERS = {':', '-'};
private static final Map<String, KeyPrefix> REGISTRY = new ConcurrentHashMap<>();
private final String prefix;
private final Provenance provenance;
private final String description;
private KeyPrefix(String prefix, Provenance provenance, String description) {
if (prefix == null || prefix.isEmpty()) {
throw new IllegalArgumentException("KeyPrefix prefix must not be null or empty");
}
if (ReservedNamespaces.isTikaNative(prefix)) {
throw new IllegalArgumentException("'" + prefix + "' is in the reserved Tika-native "
+ "namespace (tk:/X-TIKA:); it cannot be a KeyPrefix");
}
if (!hasTrailingDelimiter(prefix)) {
throw new IllegalArgumentException("'" + prefix + "' must end with one of "
+ Arrays.toString(TRAILING_DELIMITERS));
}
this.prefix = prefix;
this.provenance = provenance;
this.description = description;
}
/**
* Registers a freshly-constructed instance, tolerating an IDENTICAL redeclaration: a
* declaring class re-initialized against a shared tika-core (webapp redeploy, a second
* plugin classloader) gets the incumbent back instead of an
* {@code ExceptionInInitializerError} that kills the class until JVM restart. A
* CONFLICTING redeclaration (different provenance or description) still throws ��� that is
* either per-parse misconstruction or two libraries claiming one prefix.
*/
private static KeyPrefix register(KeyPrefix fresh) {
KeyPrefix incumbent = REGISTRY.putIfAbsent(fresh.prefix, fresh);
if (incumbent == null) {
return fresh;
}
if (incumbent.provenance == fresh.provenance
&& Objects.equals(incumbent.description, fresh.description)) {
return incumbent;
}
throw new IllegalStateException(
"KeyPrefix '" + fresh.prefix + "' is already registered with different "
+ "provenance/description; another library may already own this prefix. "
+ "KeyPrefix instances are declaration-time constants (never from "
+ "document-derived text, never per-parse).");
}
private static boolean hasTrailingDelimiter(String prefix) {
char last = prefix.charAt(prefix.length() - 1);
for (char d : TRAILING_DELIMITERS) {
if (d == last) {
return true;
}
}
return false;
}
/**
* Declares a FILE-provenance prefix: names read from the document itself. An identical
* redeclaration (same provenance and description, e.g. class re-initialization in another
* classloader) returns the incumbent instance.
*
* @throws IllegalArgumentException if {@code prefix} is null, empty, reserved, or lacks a
* trailing delimiter
* @throws IllegalStateException if {@code prefix} is already registered with a different
* provenance or description
*/
public static KeyPrefix file(String prefix, String description) {
return register(new KeyPrefix(prefix, Provenance.FILE, description));
}
/**
* Declares a TOOL-provenance prefix: names coined by an external tool or service. An
* identical redeclaration returns the incumbent instance; see {@link #file}.
*
* @throws IllegalArgumentException if {@code prefix} is null, empty, reserved, or lacks a
* trailing delimiter
* @throws IllegalStateException if {@code prefix} is already registered with a different
* provenance or description
*/
public static KeyPrefix tool(String prefix, String description) {
return register(new KeyPrefix(prefix, Provenance.TOOL, description));
}
/** The registered prefix instance for {@code prefix}, or {@code null}. */
public static KeyPrefix get(String prefix) {
return REGISTRY.get(prefix);
}
/**
* The full key for a source-derived {@code suffix}.
*
* @throws IllegalArgumentException if {@code suffix} is null or empty
*/
public String key(String suffix) {
requireNonEmptyName(suffix);
return prefix + suffix;
}
public String prefix() {
return prefix;
}
public Provenance provenance() {
return provenance;
}
public String description() {
return description;
}
/** The prefix, so a String���KeyPrefix flip doesn't turn existing
* logging/concatenation into {@code KeyPrefix@<hex>} with no compile signal. */
public String toString() {
return prefix;
}
/** Declared prefixes, from loaded classes only. */
public static Collection<KeyPrefix> registered() {
return Collections.unmodifiableCollection(REGISTRY.values());
}
private static void requireNonEmptyName(String name) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("KeyPrefix key suffix must not be null or empty");
}
}
}