PluginInfo.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.plugin;

import org.apache.doris.common.io.Text;
import org.apache.doris.common.io.Writable;
import org.apache.doris.common.util.DigitalVersion;
import org.apache.doris.persist.gson.GsonUtils;

import com.google.common.base.Strings;
import com.google.common.collect.Maps;
import com.google.gson.annotations.SerializedName;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.function.Function;
import java.util.stream.Collectors;

public class PluginInfo implements Writable {
    public static final Logger LOG = LoggerFactory.getLogger(PluginInfo.class);

    private static final String DEFAULT_PLUGIN_PROPERTIES = "plugin.properties";

    /**
     * Describe the type of plugin
     */
    public enum PluginType {
        AUDIT,
        IMPORT,
        STORAGE,
        DIALECT;

        public static int MAX_PLUGIN_TYPE_SIZE = PluginType.values().length;
    }

    @SerializedName("name")
    protected String name;

    @SerializedName("type")
    protected PluginType type;

    @SerializedName("description")
    protected String description;

    @SerializedName("version")
    protected DigitalVersion version;

    @SerializedName("javaVersion")
    protected DigitalVersion javaVersion;

    @SerializedName("className")
    protected String className;

    @SerializedName("soName")
    protected String soName;

    // this source field is only used for persisting. it should be passed to the source field in 'PluginLoader',
    // and then use 'source' in 'PluginLoader' to get the source.
    @SerializedName("source")
    protected String source;

    @SerializedName("properties")
    protected Map<String, String> properties = Maps.newHashMap();

    public PluginInfo() { }

    // used for persisting uninstall operation
    public PluginInfo(String name) {
        this.name = name;
    }

    public PluginInfo(String name, PluginType type, String description) {
        this.name = name;
        this.type = type;
        this.description = description;
        this.version = DigitalVersion.CURRENT_PLUGIN_VERSION;
        this.javaVersion = DigitalVersion.JDK_1_8_0;
    }

    public PluginInfo(String name, PluginType type, String description, DigitalVersion version,
                         DigitalVersion javaVersion,
                         String className, String soName, String source) {

        this.name = name;
        this.type = type;
        this.description = description;
        this.version = version;
        this.javaVersion = javaVersion;
        this.className = className;
        this.soName = soName;
        this.source = source;
    }

    public static PluginInfo readFromProperties(final Path propertiesPath, final String source) throws IOException {
        final Path descriptor = propertiesPath.resolve(DEFAULT_PLUGIN_PROPERTIES);
        if (!descriptor.toFile().exists()) {
            throw new IOException(descriptor.getFileName() + " does not exist");
        }

        final Map<String, String> propsMap;
        final Properties props = new Properties();
        try (InputStream stream = Files.newInputStream(descriptor)) {
            props.load(stream);
        }
        propsMap = props.stringPropertyNames().stream()
                .collect(Collectors.toMap(Function.identity(), props::getProperty));

        final String name = propsMap.remove("name");
        if (Strings.isNullOrEmpty(name)) {
            throw new IllegalArgumentException(
                    "property [name] is missing in [" + descriptor + "]");
        }

        final String description = propsMap.remove("description");

        final PluginType type;
        final String typeStr = propsMap.remove("type");
        try {
            type = PluginType.valueOf(StringUtils.upperCase(typeStr));
        } catch (IllegalArgumentException e) {
            throw new IllegalArgumentException("property [type] is missing for plugin [" + typeStr + "]");
        }

        final String versionString = propsMap.remove("version");
        if (null == versionString) {
            throw new IllegalArgumentException(
                    "property [version] is missing for plugin [" + name + "]");
        }

        DigitalVersion version = DigitalVersion.fromString(versionString);

        final String javaVersionString = propsMap.remove("java.version");
        DigitalVersion javaVersion = DigitalVersion.JDK_1_8_0;
        if (null != javaVersionString) {
            javaVersion = DigitalVersion.fromString(javaVersionString);
        }

        final String className = propsMap.remove("classname");

        final String soName = propsMap.remove("soname");

        // version check
        if (version.before(DigitalVersion.CURRENT_DORIS_VERSION)) {
            throw new IllegalArgumentException("plugin version is too old. plz recompile and modify property "
                    + "[version]");
        }

        if (!Strings.isNullOrEmpty(soName)) {
            throw new IllegalArgumentException("Only support FE plugin");
        }

        if (Strings.isNullOrEmpty(className)) {
            throw new IllegalArgumentException("property [className] is missing for plugin [" + name + "]");
        }

        return new PluginInfo(name, type, description, version, javaVersion, className, soName, source);
    }

    public String getName() {
        return name;
    }

    public PluginType getType() {
        return type;
    }

    public int getTypeId() {
        return type.ordinal();
    }

    public String getDescription() {
        return description;
    }

    public DigitalVersion getVersion() {
        return version;
    }

    public DigitalVersion getJavaVersion() {
        return javaVersion;
    }

    public String getClassName() {
        return className;
    }

    public String getSoName() {
        return soName;
    }

    public String getSource() {
        return source;
    }

    public void setProperties(Map<String, String> properties) {
        this.properties = properties;
    }

    public Map<String, String> getProperties() {
        return properties;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        PluginInfo that = (PluginInfo) o;
        return Objects.equals(name, that.name)
                && type == that.type
                && Objects.equals(version, that.version);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }

    @Override
    public void write(DataOutput out) throws IOException {
        String s = GsonUtils.GSON.toJson(this);
        Text.writeString(out, s);
    }

    public static PluginInfo read(DataInput in) throws IOException {
        String s = Text.readString(in);
        return GsonUtils.GSON.fromJson(s, PluginInfo.class);
    }
}