OperatorOnlyGuard.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.serialization;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyName;
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
import org.apache.tika.config.OperatorOnly;
/**
* Refuses per-request JSON that names an {@link OperatorOnly} property. Runs on the JSON
* tree before deserialization, so no setter sees the value. Properties are matched the way
* Jackson will bind them (declared name and aliases); a key Jackson does not know is left
* for Jackson to refuse. Object values are checked against the nested property's type.
*/
public final class OperatorOnlyGuard {
private record Property(boolean operatorOnly, JavaType type) { }
private static final Map<JavaType, Map<String, Property>> PROPERTIES = new ConcurrentHashMap<>();
private OperatorOnlyGuard() {
}
/**
* @throws IOException if {@code json} names an operator-only property of {@code configClass}
*/
public static void check(ObjectMapper mapper, String configKey, String json, Class<?> configClass)
throws IOException {
check(mapper, configKey, mapper.readTree(json), mapper.constructType(configClass));
}
/**
* @throws IOException if {@code node} names an operator-only property of {@code type}
*/
public static void check(ObjectMapper mapper, String configKey, JsonNode node, JavaType type)
throws IOException {
if (node == null || !node.isObject()) {
return;
}
Map<String, Property> properties = PROPERTIES.computeIfAbsent(type, t -> introspect(mapper, t));
Iterator<Map.Entry<String, JsonNode>> fields = node.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> e = fields.next();
Property property = properties.get(e.getKey());
if (property == null) {
continue;
}
if (property.operatorOnly()) {
throw new IOException("Cannot modify " + e.getKey() + " at runtime: per-request config for '"
+ configKey + "' may not set it. Set it in tika-config.json or a preset; "
+ "Java code that writes its own config wraps it with JsonConfig.trusted(json).");
}
if (e.getValue().isObject() && !property.type().isContainerType()) {
check(mapper, configKey, e.getValue(), property.type());
}
}
}
private static Map<String, Property> introspect(ObjectMapper mapper, JavaType type) {
BeanDescription bean = mapper.getDeserializationConfig().introspect(type);
Map<String, Property> properties = new HashMap<>();
for (BeanPropertyDefinition def : bean.findProperties()) {
Property property = new Property(isOperatorOnly(def), def.getPrimaryType());
properties.put(def.getName(), property);
for (PropertyName alias : def.findAliases()) {
properties.put(alias.getSimpleName(), property);
}
}
return properties;
}
private static boolean isOperatorOnly(BeanPropertyDefinition def) {
for (AnnotatedMember member : new AnnotatedMember[]{def.getSetter(), def.getGetter(), def.getField()}) {
if (member != null && isAnnotated(member.getMember())) {
return true;
}
}
return false;
}
private static boolean isAnnotated(Member member) {
if (member instanceof Field field) {
return field.isAnnotationPresent(OperatorOnly.class);
}
if (member instanceof Method method) {
// an override that drops the annotation must not unlock the property
for (Class<?> c = method.getDeclaringClass(); c != null; c = c.getSuperclass()) {
try {
if (c.getDeclaredMethod(method.getName(), method.getParameterTypes())
.isAnnotationPresent(OperatorOnly.class)) {
return true;
}
} catch (NoSuchMethodException ignored) {
// not declared at this level
}
}
}
return false;
}
}