content: package com.ctrip.framework.apollo.portal.component.txtresolver;
  
  import com.ctrip.framework.apollo.common.dto.ItemChangeSets;
  import com.ctrip.framework.apollo.common.dto.ItemDTO;
  import com.ctrip.framework.apollo.common.exception.BadRequestException;
  import com.ctrip.framework.apollo.common.utils.BeanUtils;
  
  import org.springframework.stereotype.Component;
  
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  
  /**
   * normal property file resolver.
   * update comment and blank item implement by create new item and delete old item.
   * update normal key/value item implement by update.
   */
  @Component("propertyResolver")
  public class PropertyResolver implements ConfigTextResolver {
  
    private static final String KV_SEPARATOR = "=";
    private static final String ITEM_SEPARATOR = "\n";
  
    @Override
    public ItemChangeSets resolve(long namespaceId, String configText, List<ItemDTO> baseItems) {
  
      Map<Integer, ItemDTO> oldLineNumMapItem = BeanUtils.mapByKey("lineNum", baseItems);
      Map<String, ItemDTO> oldKeyMapItem = BeanUtils.mapByKey("key", baseItems);
  
      //remove comment and blank item map.
      oldKeyMapItem.remove("");
  
      String[] newItems = configText.split(ITEM_SEPARATOR);
  
      if (isHasRepeatKey(newItems)) {
        throw new BadRequestException("config text has repeat key please check.");
      }
  
      ItemChangeSets changeSets = new ItemChangeSets();
      Map<Integer, String> newLineNumMapItem = new HashMap<Integer, String>();//use for delete blank and comment item
      int lineCounter = 1;
      for (String newItem : newItems) {
        newItem = newItem.trim();
        newLineNumMapItem.put(lineCounter, newItem);
        ItemDTO oldItemByLine = oldLineNumMapItem.get(lineCounter);
  
        //comment item
        if (isCommentItem(newItem)) {
  
          handleCommentLine(namespaceId, oldItemByLine, newItem, lineCounter, changeSets);
  
          //blank item
        } else if (isBlankItem(newItem)) {
  
          handleBlankLine(namespaceId, oldItemByLine, lineCounter, changeSets);
  
          //normal item
        } else {
          handleNormalLine(namespaceId, oldKeyMapItem, newItem, lineCounter, changeSets);
        }
  
        lineCounter++;
      }
  
      deleteCommentAndBlankItem(oldLineNumMapItem, newLineNumMapItem, changeSets);
      deleteNormalKVItem(oldKeyMapItem, changeSets);
  
      return changeSets;
    }
  
    private boolean isHasRepeatKey(String[] newItems) {
      Set<String> keys = new HashSet<>();
      int lineCounter = 1;
      int keyCount = 0;
      for (String item : newItems) {
        if (!isCommentItem(item) && !isBlankItem(item)) {
          keyCount++;
          String[] kv = parseKeyValueFromItem(item);
          if (kv != null) {
            keys.add(kv[0]);
          } else {
            throw new BadRequestException("line:" + lineCounter + " key value must separate by '='");
          }
        }
        lineCounter++;
      }
  
      return keyCount > keys.size();
    }
  
    private String[] parseKeyValueFromItem(String item) {
      int kvSeparator = item.indexOf(KV_SEPARATOR);
      if (kvSeparator == -1) {
        return null;
      }
  
      String[] kv = new String[2];
      kv[0] = item.substring(0, kvSeparator).trim();
      kv[1] = item.substring(kvSeparator + 1, item.length()).trim();
      return kv;
    }
  
    private void handleCommentLine(Long namespaceId, ItemDTO oldItemByLine, String newItem, int lineCounter, ItemChangeSets changeSets) {
      String oldComment = oldItemByLine == null ? "" : oldItemByLine.getComment();
      //create comment. implement update comment by delete old comment and create new comment
      if (!(isCommentItem(oldItemByLine) && newItem.equals(oldComment))) {
        changeSets.addCreateItem(buildCommentItem(0l, namespaceId, newItem, lineCounter));
      }
    }
  
    private void handleBlankLine(Long namespaceId, ItemDTO oldItem, int lineCounter, ItemChangeSets changeSets) {
      if (!isBlankItem(oldItem)) {
        changeSets.addCreateItem(buildBlankItem(0l, namespaceId, lineCounter));
      }
    }
  
    private void handleNormalLine(Long namespaceId, Map<String, ItemDTO> keyMapOldItem, String newItem,
                                  int lineCounter, ItemChangeSets changeSets) {
  
      String[] kv = parseKeyValueFromItem(newItem);
  
      if (kv == null) {
        throw new BadRequestException("line:" + lineCounter + " key value must separate by '='");
      }
  
      String newKey = kv[0];
      String newValue = kv[1].replace("\\n", "\n"); //handle user input \n
  
      ItemDTO oldItem = keyMapOldItem.get(newKey);
  
      if (oldItem == null) {//new item
        changeSets.addCreateItem(buildNormalItem(0l, namespaceId, newKey, newValue, "", lineCounter));
      } else if (!newValue.equals(oldItem.getValue()) || lineCounter != oldItem.getLineNum()) {//update item
        changeSets.addUpdateItem(
            buildNormalItem(oldItem.getId(), namespaceId, newKey, newValue, oldItem.getComment(),
                lineCounter));
      }
      keyMapOldItem.remove(newKey);
    }
  
    private boolean isCommentItem(ItemDTO item) {
      return item != null && "".equals(item.getKey())
          && (item.getComment().startsWith("#") || item.getComment().startsWith("!"));
    }
  
    private boolean isCommentItem(String line) {
      return line != null && (line.startsWith("#") || line.startsWith("!"));
    }
  
    private boolean isBlankItem(ItemDTO item) {
      return item != null && "".equals(item.getKey()) && "".equals(item.getComment());
    }
  
    private boolean isBlankItem(String line) {
      return "".equals(line);
    }
  
    private void deleteNormalKVItem(Map<String, ItemDTO> baseKeyMapItem, ItemChangeSets changeSets) {
      //surplus item is to be deleted
      for (Map.Entry<String, ItemDTO> entry : baseKeyMapItem.entrySet()) {
        changeSets.addDeleteItem(entry.getValue());
      }
    }
  
    private void deleteCommentAndBlankItem(Map<Integer, ItemDTO> oldLineNumMapItem,
                                           Map<Integer, String> newLineNumMapItem,
                                           ItemChangeSets changeSets) {
  
      for (Map.Entry<Integer, ItemDTO> entry : oldLineNumMapItem.entrySet()) {
        int lineNum = entry.getKey();
        ItemDTO oldItem = entry.getValue();
        String newItem = newLineNumMapItem.get(lineNum);
  
        //1. old is blank by now is not
        //2.old is comment by now is not exist or modified
        if ((isBlankItem(oldItem) && !isBlankItem(newItem))
            || isCommentItem(oldItem) && (newItem == null || !newItem.equals(oldItem.getComment()))) {
          changeSets.addDeleteItem(oldItem);
        }
      }
    }
  
    private ItemDTO buildCommentItem(Long id, Long namespaceId, String comment, int lineNum) {
      return buildNormalItem(id, namespaceId, "", "", comment, lineNum);
    }
  
    private ItemDTO buildBlankItem(Long id, Long namespaceId, int lineNum) {
      return buildNormalItem(id, namespaceId, "", "", "", lineNum);
    }
  
    private ItemDTO buildNormalItem(Long id, Long namespaceId, String key, String value, String comment, int lineNum) {
      ItemDTO item = new ItemDTO(key, value, comment, lineNum);
      item.setId(id);
      item.setNamespaceId(namespaceId);
      return item;
    }
  }
  