content: /*
   * Copyright (C) 2016-2023 ActionTech.
   * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher.
   */
  
  package com.actiontech.dble.services.manager.information.tables;
  
  import com.actiontech.dble.DbleServer;
  import com.actiontech.dble.backend.datasource.PhysicalDbGroup;
  import com.actiontech.dble.cluster.path.ClusterPathUtil;
  import com.actiontech.dble.cluster.values.RawJson;
  import com.actiontech.dble.cluster.zkprocess.entity.Users;
  import com.actiontech.dble.cluster.zkprocess.entity.user.RwSplitUser;
  import com.actiontech.dble.config.ConfigFileName;
  import com.actiontech.dble.config.DbleTempConfig;
  import com.actiontech.dble.config.ErrorCode;
  import com.actiontech.dble.config.Fields;
  import com.actiontech.dble.config.converter.UserConverter;
  import com.actiontech.dble.config.model.user.RwSplitUserConfig;
  import com.actiontech.dble.config.model.user.UserConfig;
  import com.actiontech.dble.config.util.ConfigException;
  import com.actiontech.dble.meta.ColumnMeta;
  import com.actiontech.dble.services.manager.information.ManagerWritableTable;
  import com.actiontech.dble.util.*;
  import com.google.common.collect.Maps;
  
  import java.io.File;
  import java.sql.SQLException;
  import java.util.*;
  import java.util.stream.Collectors;
  
  public class DbleRwSplitEntry extends ManagerWritableTable {
  
      public static final String TABLE_NAME = "dble_rw_split_entry";
  
      private static final String COLUMN_ID = "id";
      private static final String COLUMN_TYPE = "type";
      public static final String COLUMN_USERNAME = "username";
      private static final String COLUMN_PASSWORD_ENCRYPT = "password_encrypt";
      private static final String COLUMN_ENCRYPT_CONFIGURED = "encrypt_configured";
      public static final String COLUMN_CONN_ATTR_KEY = "conn_attr_key";
      public static final String COLUMN_CONN_ATTR_VALUE = "conn_attr_value";
      private static final String COLUMN_WHITE_IPS = "white_ips";
      private static final String COLUMN_MAX_CONN_COUNT = "max_conn_count";
      private static final String COLUMN_BLACKLIST = "blacklist";
      public static final String COLUMN_DB_GROUP = "db_group";
      public static final String CONSTANT_NO_LIMIT = "no limit";
  
      public DbleRwSplitEntry() {
          super(TABLE_NAME, 10);
          setNotWritableColumnSet(COLUMN_ID, COLUMN_BLACKLIST, COLUMN_TYPE);
          setLogicalPrimaryKeySet(COLUMN_USERNAME, COLUMN_CONN_ATTR_KEY, COLUMN_CONN_ATTR_VALUE);
          String path = ResourceUtil.getResourcePathFromRoot(ClusterPathUtil.LOCAL_WRITE_PATH);
          path = new File(path).getPath() + File.separator + ConfigFileName.USER_XML;
          this.setXmlFilePath(path);
      }
  
      @Override
      protected void initColumnAndType() {
          columns.put(COLUMN_ID, new ColumnMeta(COLUMN_ID, "int(11)", true, true));
          columnsType.put(COLUMN_ID, Fields.FIELD_TYPE_LONG);
  
          columns.put(COLUMN_TYPE, new ColumnMeta(COLUMN_TYPE, "varchar(9)", true));
          columnsType.put(COLUMN_TYPE, Fields.FIELD_TYPE_VAR_STRING);
  
          columns.put(COLUMN_USERNAME, new ColumnMeta(COLUMN_USERNAME, "varchar(64)", false));
          columnsType.put(COLUMN_USERNAME, Fields.FIELD_TYPE_VAR_STRING);
  
          columns.put(COLUMN_PASSWORD_ENCRYPT, new ColumnMeta(COLUMN_PASSWORD_ENCRYPT, "varchar(200)", false));
          columnsType.put(COLUMN_PASSWORD_ENCRYPT, Fields.FIELD_TYPE_VAR_STRING);
  
          columns.put(COLUMN_ENCRYPT_CONFIGURED, new ColumnMeta(COLUMN_ENCRYPT_CONFIGURED, "varchar(5)", true, "true"));
          columnsType.put(COLUMN_ENCRYPT_CONFIGURED, Fields.FIELD_TYPE_VAR_STRING);
  
          columns.put(COLUMN_CONN_ATTR_KEY, new ColumnMeta(COLUMN_CONN_ATTR_KEY, "varchar(6)", true));
          columnsType.put(COLUMN_CONN_ATTR_KEY, Fields.FIELD_TYPE_VAR_STRING);
  
          columns.put(COLUMN_CONN_ATTR_VALUE, new ColumnMeta(COLUMN_CONN_ATTR_VALUE, "varchar(64)", true));
          columnsType.put(COLUMN_CONN_ATTR_VALUE, Fields.FIELD_TYPE_VAR_STRING);
  
          columns.put(COLUMN_WHITE_IPS, new ColumnMeta(COLUMN_WHITE_IPS, "varchar(200)", true));
          columnsType.put(COLUMN_WHITE_IPS, Fields.FIELD_TYPE_VAR_STRING);
  
          columns.put(COLUMN_MAX_CONN_COUNT, new ColumnMeta(COLUMN_MAX_CONN_COUNT, "varchar(64)", false));
          columnsType.put(COLUMN_MAX_CONN_COUNT, Fields.FIELD_TYPE_VAR_STRING);
  
          columns.put(COLUMN_BLACKLIST, new ColumnMeta(COLUMN_BLACKLIST, "varchar(64)", true));
          columnsType.put(COLUMN_BLACKLIST, Fields.FIELD_TYPE_VAR_STRING);
  
          columns.put(COLUMN_DB_GROUP, new ColumnMeta(COLUMN_DB_GROUP, "varchar(64)", false));
          columnsType.put(COLUMN_DB_GROUP, Fields.FIELD_TYPE_VAR_STRING);
      }
  
      @Override
      protected List<LinkedHashMap<String, String>> getRows() {
          List<LinkedHashMap<String, String>> list = new ArrayList<>();
          Set<String> dbGroups = DbleServer.getInstance().getConfig().getDbGroups().keySet();
          DbleServer.getInstance().getConfig().getUsers().entrySet().stream().sorted(Comparator.comparingInt(a -> a.getValue().getId())).forEach(v -> {
              UserConfig userConfig = v.getValue();
              if (userConfig instanceof RwSplitUserConfig) {
                  RwSplitUserConfig rwSplitUserConfig = (RwSplitUserConfig) userConfig;
                  String dbGroupName = rwSplitUserConfig.getDbGroup();
                  if (dbGroups.contains(dbGroupName)) {
                      LinkedHashMap<String, String> map = Maps.newLinkedHashMap();
                      map.put(COLUMN_ID, userConfig.getId() + "");
                      map.put(COLUMN_TYPE, rwSplitUserConfig.getTenant() != null ? "conn_attr" : "username");
                      map.put(COLUMN_USERNAME, rwSplitUserConfig.getName());
                      map.put(COLUMN_PASSWORD_ENCRYPT, DbleEntry.getPasswordEncrypt(rwSplitUserConfig));
                      map.put(COLUMN_ENCRYPT_CONFIGURED, String.valueOf(rwSplitUserConfig.isEncrypt()));
                      map.put(COLUMN_CONN_ATTR_KEY, rwSplitUserConfig.getTenant() != null ? "tenant" : null);
                      map.put(COLUMN_CONN_ATTR_VALUE, rwSplitUserConfig.getTenant());
                      map.put(COLUMN_WHITE_IPS, DbleEntry.getWhiteIps(rwSplitUserConfig.getWhiteIPs()));
                      map.put(COLUMN_MAX_CONN_COUNT, rwSplitUserConfig.getMaxCon() == 0 ? CONSTANT_NO_LIMIT : rwSplitUserConfig.getMaxCon() + "");
                      map.put(COLUMN_BLACKLIST, rwSplitUserConfig.getBlacklist() == null ? null : rwSplitUserConfig.getBlacklist().getName());
                      map.put(COLUMN_DB_GROUP, dbGroupName);
                      list.add(map);
                  }
              }
          });
          return list;
      }
  
      @Override
      public int insertRows(List<LinkedHashMap<String, String>> rows) throws SQLException {
          List<RwSplitUser> rwSplitUserList = rows.stream().map(this::transformRowToUser).collect(Collectors.toList());
  
          UserConverter userConverter = new UserConverter();
          RawJson userConfig = DbleServer.getInstance().getConfig().getUserConfig();
          Users users = userConverter.userJsonToBean(userConfig);
  
          checkLogicalUniqueKeyDuplicate(users, rwSplitUserList);
  
          users.getUser().addAll(rwSplitUserList);
          userConfig = userConverter.userBeanToJson(users);
          DbleTempConfig.getInstance().setUserConfig(userConfig);
          return rows.size();
      }
  
  
      @Override
      public int updateRows(Set<LinkedHashMap<String, String>> affectPks, LinkedHashMap<String, String> values) {
          affectPks.forEach(affectPk -> {
              if (Boolean.FALSE.toString().equalsIgnoreCase(affectPk.get(COLUMN_ENCRYPT_CONFIGURED))) {
                  String password = DecryptUtil.decrypt(true, affectPk.get(COLUMN_USERNAME), affectPk.get(COLUMN_PASSWORD_ENCRYPT));
                  affectPk.put(COLUMN_PASSWORD_ENCRYPT, password);
              }
              affectPk.putAll(values);
          });
          List<RwSplitUser> rwSplitUserList = affectPks.stream().map(this::transformRowToUser).collect(Collectors.toList());
  
          UserConverter userConverter = new UserConverter();
          RawJson userConfig = DbleServer.getInstance().getConfig().getUserConfig();
          Users users = userConverter.userJsonToBean(userConfig);
  
          updateList(users, rwSplitUserList, false);
          userConfig = userConverter.userBeanToJson(users);
          DbleTempConfig.getInstance().setUserConfig(userConfig);
  
          return affectPks.size();
      }
  
      @Override
      public int deleteRows(Set<LinkedHashMap<String, String>> affectPks) {
          List<RwSplitUser> rwSplitUserList = affectPks.stream().map(this::transformRowToUser).collect(Collectors.toList());
  
          UserConverter userConverter = new UserConverter();
          RawJson userConfig = DbleServer.getInstance().getConfig().getUserConfig();
          Users users = userConverter.userJsonToBean(userConfig);
  
          updateList(users, rwSplitUserList, true);
          userConfig = userConverter.userBeanToJson(users);
          DbleTempConfig.getInstance().setUserConfig(userConfig);
          return affectPks.size();
      }
  
  
      private void checkLogicalUniqueKeyDuplicate(Users users, List<RwSplitUser> rwSplitUserList) throws SQLException {
          List<RwSplitUser> sourceList = users.getUser().stream().filter(user -> user instanceof RwSplitUser).map(user -> (RwSplitUser) user).collect(Collectors.toList());
          for (RwSplitUser rwSplitUser : rwSplitUserList) {
              boolean isExist = sourceList.stream().anyMatch(sourceUser -> StringUtil.equals(sourceUser.getName(), rwSplitUser.getName()) && StringUtil.equals(sourceUser.getTenant(), rwSplitUser.getTenant()));
              if (isExist) {
                  String msg = String.format("Duplicate entry '%s-%s-%s'for logical unique '%s-%s-%s'", rwSplitUser.getName(),
                          StringUtil.isEmpty(rwSplitUser.getTenant()) ? null : "tenant", rwSplitUser.getTenant(), COLUMN_USERNAME, COLUMN_CONN_ATTR_KEY, COLUMN_CONN_ATTR_VALUE);
                  throw new SQLException(msg, "42S22", ErrorCode.ER_DUP_ENTRY);
              }
          }
      }
  
      private void updateList(Users users, List<RwSplitUser> rwSplitUserList, boolean isDelete) {
          for (RwSplitUser rwSplitUser : rwSplitUserList) {
              for (int i = 0; i < users.getUser().size(); i++) {
                  Object obj = users.getUser().get(i);
                  if (obj instanceof RwSplitUser) {
                      RwSplitUser sourceUser = (RwSplitUser) obj;
                      if (StringUtil.equals(sourceUser.getName(), rwSplitUser.getName()) && StringUtil.equals(sourceUser.getTenant(), rwSplitUser.getTenant())) {
                          if (!isDelete) {
                              users.getUser().set(i, rwSplitUser);
                          } else {
                              users.getUser().remove(i);
                          }
                          break;
                      }
                  }
              }
          }
      }
  
      private void check(LinkedHashMap<String, String> tempRowMap) {
          //check whiteIPs
          checkWhiteIPs(tempRowMap);
          //check db_group
          checkDbGroup(tempRowMap);
          //check boolean
          checkBooleanVal(tempRowMap);
      }
  
      private void checkBooleanVal(LinkedHashMap<String, String> tempRowMap) {
          if (tempRowMap.containsKey(COLUMN_ENCRYPT_CONFIGURED) && !StringUtil.isEmpty(tempRowMap.get(COLUMN_ENCRYPT_CONFIGURED))) {
              String encryptConfigured = tempRowMap.get(COLUMN_ENCRYPT_CONFIGURED);
              if (!StringUtil.equalsIgnoreCase(encryptConfigured, Boolean.FALSE.toString()) && !StringUtil.equalsIgnoreCase(encryptConfigured, Boolean.TRUE.toString())) {
                  throw new ConfigException("Column 'encrypt_configured' values only support 'false' or 'true'.");
              }
          }
          if (!StringUtil.isBlank(tempRowMap.get(COLUMN_CONN_ATTR_KEY))) {
              if (!StringUtil.equals(tempRowMap.get(COLUMN_CONN_ATTR_KEY), "tenant")) {
                  throw new ConfigException("'conn_attr_key' value is ['tenant',null].");
              }
              if (StringUtil.isBlank(tempRowMap.get(COLUMN_CONN_ATTR_VALUE))) {
                  throw new ConfigException("'conn_attr_key' and 'conn_attr_value' are used together.");
              }
          } else {
              if (!StringUtil.isBlank(tempRowMap.get(COLUMN_CONN_ATTR_VALUE))) {
                  throw new ConfigException("'conn_attr_key' and 'conn_attr_value' are used together.");
              }
          }
      }
  
  
      private void checkWhiteIPs(LinkedHashMap<String, String> tempRowMap) {
          if (tempRowMap.containsKey(COLUMN_WHITE_IPS) && !StringUtil.isEmpty(tempRowMap.get(COLUMN_WHITE_IPS))) {
              IPAddressUtil.checkWhiteIPs(tempRowMap.get(COLUMN_WHITE_IPS));
          }
      }
  
      private void checkDbGroup(LinkedHashMap<String, String> tempRowMap) {
          if (tempRowMap.containsKey(COLUMN_DB_GROUP)) {
              Map<String, PhysicalDbGroup> dbGroupMap = DbleServer.getInstance().getConfig().getDbGroups();
              boolean isExist = dbGroupMap.keySet().stream().anyMatch(groupName -> StringUtil.equals(groupName, tempRowMap.get(COLUMN_DB_GROUP)));
              if (!isExist) {
                  throw new ConfigException("Column 'db_group' value '" + tempRowMap.get(COLUMN_DB_GROUP) + "' does not exist or not active.");
              }
          }
      }
  
      private RwSplitUser transformRowToUser(LinkedHashMap<String, String> map) {
          if (null == map || map.isEmpty()) {
              return null;
          }
          check(map);
          RwSplitUser rwSplitUser = new RwSplitUser();
          for (Map.Entry<String, String> entry : map.entrySet()) {
              switch (entry.getKey()) {
                  case COLUMN_USERNAME:
                      rwSplitUser.setName(entry.getValue());
                      break;
                  case COLUMN_PASSWORD_ENCRYPT:
                      rwSplitUser.setPassword(entry.getValue());
                      break;
                  case COLUMN_ENCRYPT_CONFIGURED:
                      rwSplitUser.setUsingDecrypt(entry.getValue());
                      break;
                  case COLUMN_CONN_ATTR_VALUE:
                      rwSplitUser.setTenant(entry.getValue());
                      break;
                  case COLUMN_WHITE_IPS:
                      rwSplitUser.setWhiteIPs(entry.getValue());
                      break;
                  case COLUMN_MAX_CONN_COUNT:
                      if (!StringUtil.isBlank(entry.getValue())) {
                          rwSplitUser.setMaxCon(IntegerUtil.parseInt(entry.getValue().replace(CONSTANT_NO_LIMIT, "0")));
                      }
                      if (rwSplitUser.getMaxCon() < 0) {
                          throw new ConfigException("Column 'max_conn_count' value cannot be less than 0.");
                      }
                      break;
                  case COLUMN_DB_GROUP:
                      rwSplitUser.setDbGroup(entry.getValue());
                      break;
                  default:
                      break;
              }
          }
          return rwSplitUser;
      }
  
      @Override
      public void updateTempConfig() {
          DbleTempConfig.getInstance().setUserConfig(DbleServer.getInstance().getConfig().getUserConfig());
      }
  }
  