StandardMetadataLimiterFactory.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.writelimiter;

import java.util.HashSet;
import java.util.Set;

import org.apache.tika.annotation.TikaComponent;

/**
 * Standard factory for creating {@link StandardMetadataLimiter} instances.
 * <p>
 * This factory holds configuration for metadata size limits and field prioritization.
 * It creates limiter instances that enforce these limits when metadata is written.
 * <p>
 * <b>Size limits</b> prevent memory exhaustion from malicious/pathological files:
 * <ul>
 *   <li>{@code maxKeySize} - maximum metadata key length in UTF-16 bytes</li>
 *   <li>{@code maxFieldSize} - maximum value size per field in UTF-16 bytes</li>
 *   <li>{@code maxTotalEstimatedBytes} - total metadata budget in UTF-16 bytes</li>
 *   <li>{@code maxValuesPerField} - maximum number of values per multi-valued field</li>
 * </ul>
 * <p>
 * <b>Prioritization fields</b> reduce the likelihood of hitting limits:
 * <ul>
 *   <li>{@code includeFields} - if non-empty, ONLY these fields are stored</li>
 *   <li>{@code excludeFields} - these fields are never stored</li>
 *   <li>{@code includeEmpty} - whether to store empty/null values</li>
 * </ul>
 * <p>
 * See {@link StandardMetadataLimiter} for details on how sizes are calculated.
 *
 * @since Apache Tika 4.0 (renamed from StandardWriteFilterFactory)
 */
@TikaComponent(name = "standard-metadata-limiter-factory", contextKey = MetadataWriteLimiterFactory.class)
public class StandardMetadataLimiterFactory implements MetadataWriteLimiterFactory {

    public static final int DEFAULT_MAX_KEY_SIZE = 1024;
    public static final int DEFAULT_MAX_FIELD_SIZE = 100 * 1024;
    public static final int DEFAULT_MAX_TOTAL_BYTES = 10 * 1024 * 1024;
    public static final int DEFAULT_MAX_VALUES_PER_FIELD = 10;

    private Set<String> includeFields = new HashSet<>();
    private Set<String> excludeFields = new HashSet<>();
    private int maxKeySize = DEFAULT_MAX_KEY_SIZE;
    private int maxFieldSize = DEFAULT_MAX_FIELD_SIZE;
    private int maxTotalBytes = DEFAULT_MAX_TOTAL_BYTES;
    private int maxValuesPerField = DEFAULT_MAX_VALUES_PER_FIELD;
    private boolean includeEmpty = false;

    @Override
    public MetadataWriteLimiter newInstance() {
        if (maxFieldSize < 0) {
            throw new IllegalArgumentException("maxFieldSize must be > 0");
        }

        if (maxValuesPerField < 1) {
            throw new IllegalArgumentException("maxValuesPerField must be > 0");
        }

        if (maxTotalBytes < 0) {
            throw new IllegalArgumentException("maxTotalBytes must be > 0");
        }

        return new StandardMetadataLimiter(maxKeySize, maxFieldSize,
                maxTotalBytes, maxValuesPerField, includeFields,
                excludeFields, includeEmpty);
    }

    public void setIncludeFields(Set<String> includeFields) {
        this.includeFields = includeFields != null ? new HashSet<>(includeFields) : new HashSet<>();
    }

    public void setExcludeFields(Set<String> excludeFields) {
        this.excludeFields = excludeFields != null ? new HashSet<>(excludeFields) : new HashSet<>();
    }

    public void setMaxTotalBytes(int maxTotalBytes) {
        this.maxTotalBytes = maxTotalBytes;
    }

    public void setMaxKeySize(int maxKeySize) {
        this.maxKeySize = maxKeySize;
    }

    public void setMaxFieldSize(int maxFieldSize) {
        this.maxFieldSize = maxFieldSize;
    }

    public void setIncludeEmpty(boolean includeEmpty) {
        this.includeEmpty = includeEmpty;
    }

    public void setMaxValuesPerField(int maxValuesPerField) {
        this.maxValuesPerField = maxValuesPerField;
    }

    public Set<String> getIncludeFields() {
        return includeFields;
    }

    public Set<String> getExcludeFields() {
        return excludeFields;
    }

    public int getMaxKeySize() {
        return maxKeySize;
    }

    public int getMaxFieldSize() {
        return maxFieldSize;
    }

    public int getMaxTotalBytes() {
        return maxTotalBytes;
    }

    public int getMaxValuesPerField() {
        return maxValuesPerField;
    }

    public boolean isIncludeEmpty() {
        return includeEmpty;
    }

    @Override
    public String toString() {
        return "StandardMetadataLimiterFactory{" +
                "maxKeySize=" + maxKeySize +
                ", maxFieldSize=" + maxFieldSize +
                ", maxTotalBytes=" + maxTotalBytes +
                ", maxValuesPerField=" + maxValuesPerField +
                ", includeFields=" + includeFields +
                ", excludeFields=" + excludeFields +
                ", includeEmpty=" + includeEmpty +
                '}';
    }
}