PropertySchema.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.common.property;

import org.apache.doris.thrift.TPropertyVal;

import com.google.common.collect.ImmutableMap;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Arrays;
import java.util.Date;
import java.util.Optional;

@SuppressWarnings({"unchecked", "rawtypes"})
public abstract class PropertySchema<T> {
    private final String name;
    private final boolean required;
    private Optional<T> defaultValue = Optional.empty();
    private Optional<T> maxValue = Optional.empty();
    private Optional<T> minValue = Optional.empty();

    protected PropertySchema(String name) {
        this(name, false);
    }

    public PropertySchema(String name, boolean required) {
        this.name = name;
        this.required = required;
    }

    public static ImmutableMap<String, PropertySchema> createSchemas(PropertySchema ... schemas) {
        ImmutableMap.Builder builder = ImmutableMap.builder();
        Arrays.stream(schemas).forEach(s -> builder.put(s.getName(), s));
        return builder.build();
    }

    public interface SchemaGroup {
        ImmutableMap<String, PropertySchema> getSchemas();
    }

    public static final class StringProperty extends ComparableProperty<String> {
        StringProperty(String name) {
            super(name);
        }

        StringProperty(String name, boolean isRequired) {
            super(name, isRequired);
        }

        @Override
        public String read(String rawVal) throws IllegalArgumentException {
            verifyRange(rawVal);
            return rawVal;
        }

        @Override
        public String read(TPropertyVal tVal) throws IllegalArgumentException {
            verifyRange(tVal.getStrVal());
            return tVal.getStrVal();
        }

        @Override
        public void write(String val, TPropertyVal out) {
            out.setStrVal(val);
        }
    }

    public static final class IntProperty extends ComparableProperty<Integer> {
        IntProperty(String name) {
            super(name);
        }

        IntProperty(String name, boolean isRequired) {
            super(name, isRequired);
        }

        @Override
        public Integer read(String rawVal) {
            try {
                Integer val = Integer.parseInt(rawVal);
                verifyRange(val);
                return val;
            } catch (NumberFormatException e) {
                throw new IllegalArgumentException(String.format("Invalid integer %s: %s", rawVal, e.getMessage()));
            }
        }

        @Override
        public Integer read(TPropertyVal tVal) throws IllegalArgumentException {
            verifyRange(tVal.getIntVal());
            return tVal.getIntVal();
        }

        @Override
        public void write(Integer val, TPropertyVal out) {
            out.setIntVal(val);
        }
    }

    public static final class LongProperty extends ComparableProperty<Long> {
        LongProperty(String name) {
            super(name);
        }

        LongProperty(String name, boolean isRequired) {
            super(name, isRequired);
        }

        @Override
        public Long read(String rawVal) {
            try {
                Long val = Long.parseLong(rawVal);
                verifyRange(val);
                return val;
            } catch (NumberFormatException e) {
                throw new IllegalArgumentException(String.format("Invalid long %s: %s", rawVal, e.getMessage()));
            }
        }

        @Override
        public Long read(TPropertyVal tVal) throws IllegalArgumentException {
            verifyRange(tVal.getLongVal());
            return tVal.getLongVal();
        }

        @Override
        public void write(Long val, TPropertyVal out) {
            out.setLongVal(val);
        }
    }

    public static final class BooleanProperty extends ComparableProperty<Boolean> {
        BooleanProperty(String name) {
            super(name);
        }

        BooleanProperty(String name, boolean isRequired) {
            super(name, isRequired);
        }

        @Override
        public Boolean read(String rawVal) {
            if (rawVal == null || (!rawVal.equalsIgnoreCase("true")
                    && !rawVal.equalsIgnoreCase("false"))) {
                throw new IllegalArgumentException(String.format("Invalid boolean : %s, use true or false", rawVal));
            }

            try {
                return Boolean.parseBoolean(rawVal);
            } catch (NumberFormatException e) {
                throw new IllegalArgumentException(String.format("Invalid boolean %s: %s", rawVal, e.getMessage()));
            }
        }

        @Override
        public Boolean read(TPropertyVal tVal) throws IllegalArgumentException {
            return tVal.isBoolVal();
        }

        @Override
        public void write(Boolean val, TPropertyVal out) {
            out.setBoolVal(val);
        }
    }

    public static final class DateProperty extends PropertySchema<Date> {
        DateTimeFormatter dateFormat;

        public DateProperty(String name, DateTimeFormatter dateFormat) {
            super(name);
            this.dateFormat = dateFormat;
        }

        DateProperty(String name, DateTimeFormatter dateFormat, boolean isRequired) {
            super(name, isRequired);
            this.dateFormat = dateFormat;
        }

        @Override
        public Date read(String rawVal) throws IllegalArgumentException {
            if (rawVal == null) {
                throw new IllegalArgumentException("Invalid time format, time param can not is null");
            }
            return readTimeFormat(rawVal);
        }

        @Override
        public Date read(TPropertyVal tVal) throws IllegalArgumentException {
            return readTimeFormat(tVal.getStrVal());
        }

        @Override
        public void write(Date val, TPropertyVal out) {
            out.setStrVal(writeTimeFormat(val));
        }

        public Date readTimeFormat(String timeStr) throws IllegalArgumentException {
            try {
                return Date.from(LocalDateTime.parse(timeStr, dateFormat).atZone(ZoneId.systemDefault()).toInstant());
            } catch (DateTimeParseException e) {
                throw new IllegalArgumentException("Invalid time format, time param need "
                        + "to be " + this.dateFormat.toString());
            }
        }

        public String writeTimeFormat(Date timeDate) throws IllegalArgumentException {
            return LocalDateTime.ofInstant(timeDate.toInstant(), ZoneId.systemDefault()).format(this.dateFormat);
        }
    }

    public static final class EnumProperty<T extends Enum<T>> extends PropertySchema<T> {
        private final Class<T> enumClass;

        EnumProperty(String name, Class<T> enumClass) {
            super(name);
            this.enumClass = enumClass;
        }

        EnumProperty(String name, Class<T> enumClass, boolean isRequired) {
            super(name, isRequired);
            this.enumClass = enumClass;
        }

        @Override
        public T read(String rawVal) {
            if (rawVal == null || rawVal.length() == 0) {
                throw new IllegalArgumentException(formatError(rawVal));
            }
            try {
                return T.valueOf(enumClass, rawVal.toUpperCase());
            } catch (IllegalArgumentException e) {
                throw new IllegalArgumentException(formatError(rawVal));
            }
        }

        @Override
        public T read(TPropertyVal tVal) throws IllegalArgumentException {
            return T.valueOf(enumClass, tVal.getStrVal());
        }

        @Override
        public void write(T val, TPropertyVal out) {
            out.setStrVal(val.name());
        }

        private String formatError(String rawVal) {
            String enumsStr = Arrays.stream(enumClass.getEnumConstants())
                    .map(Enum::toString)
                    .reduce((sa, sb) -> sa + ", " + sb)
                    .orElse("");
            return String.format("Expected values are [%s], while [%s] provided", enumsStr, rawVal);
        }
    }

    private abstract static class ComparableProperty<T extends Comparable> extends PropertySchema<T> {
        protected ComparableProperty(String name) {
            super(name);
        }

        protected ComparableProperty(String name, boolean isRequired) {
            super(name, isRequired);
        }

        protected void verifyRange(T val) throws IllegalArgumentException {
            if (getMinValue().isPresent() && (val == null || getMinValue().get().compareTo(val) > 0)) {
                throw new IllegalArgumentException(val + " should not be less than " + getMinValue().get());
            }

            if (getMaxValue().isPresent() && (val == null || getMaxValue().get().compareTo(val) < 0)) {
                throw new IllegalArgumentException(val + " should not be greater than " + getMaxValue().get());
            }
        }
    }

    PropertySchema<T> setDefauleValue(T val) {
        this.defaultValue = Optional.of(val);
        return this;
    }

    PropertySchema<T> setMin(T min) {
        this.minValue = Optional.of(min);
        return this;
    }

    PropertySchema<T> setMax(T max) {
        this.maxValue = Optional.of(max);
        return this;
    }

    public String getName() {
        return name;
    }

    public boolean isRequired() {
        return required;
    }

    public Optional<T> getDefaultValue() {
        return defaultValue;
    }

    public Optional<T> getMinValue() {
        return minValue;
    }

    public Optional<T> getMaxValue() {
        return maxValue;
    }

    public abstract T read(String rawVal) throws IllegalArgumentException;

    public abstract T read(TPropertyVal tVal) throws IllegalArgumentException;

    public abstract void write(T val, TPropertyVal out);
}