Uris.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.doris.datasource.property.storage;

import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.util.JsonUtil;

import com.fasterxml.jackson.databind.JsonNode;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;

/**
 * Parses the {@code uris} property of an external-file table-valued function into a list of
 * location URIs.
 *
 * <p>{@code uris} is a JSON array naming several datasets at once. The parser validates only the
 * JSON shape of that array and throws {@link AnalysisException} when the input is malformed: a
 * non-array, an empty array, more than {@link #MAX_URIS} elements, or a non-string / blank element.
 * Duplicate elements are collapsed while the written order is preserved.
 */
public final class Uris {

    /** The property key for {@code uris}. */
    public static final String KEY = "uris";

    /**
     * The maximum number of elements allowed in {@code uris}; a longer array is refused at parse time.
     */
    public static final int MAX_URIS = 10_000;

    private Uris() {
    }

    /**
     * Parses {@code raw} into the URIs it names, in the order written and without duplicates.
     *
     * @throws AnalysisException if {@code raw} is not a JSON array, is empty, has more than
     *         {@link #MAX_URIS} elements, or contains a non-string or blank element - the message
     *         identifies the offending part
     */
    public static List<String> parse(String raw) throws AnalysisException {
        if (raw == null || raw.trim().isEmpty()) {
            throw new AnalysisException("'" + KEY + "' must be a JSON array of URIs, "
                    + "for example '[\"s3://bucket/a\", \"s3://bucket/b\"]'");
        }
        JsonNode root;
        try {
            root = JsonUtil.readTree(raw);
        } catch (RuntimeException e) {
            throw new AnalysisException("'" + KEY + "' is not valid JSON: it must be a JSON array of "
                    + "URIs, for example '[\"s3://bucket/a\", \"s3://bucket/b\"]'");
        }
        if (root == null || !root.isArray()) {
            throw new AnalysisException("'" + KEY + "' must be a JSON array of URIs");
        }
        if (root.isEmpty()) {
            throw new AnalysisException("'" + KEY + "' must name at least one location; "
                    + "the array is empty");
        }
        if (root.size() > MAX_URIS) {
            throw new AnalysisException("'" + KEY + "' must not exceed " + MAX_URIS
                    + " elements, but got " + root.size());
        }

        LinkedHashSet<String> uris = new LinkedHashSet<>();
        for (int index = 0; index < root.size(); index++) {
            JsonNode element = root.get(index);
            if (element == null || !element.isTextual()) {
                throw new AnalysisException("'" + KEY + "' element at index " + index
                        + " must be a URI string");
            }
            String uri = element.asText();
            if (uri.trim().isEmpty()) {
                throw new AnalysisException("'" + KEY + "' element at index " + index
                        + " must not be empty");
            }
            uris.add(uri);
        }
        return new ArrayList<>(uris);
    }
}