code: package com.beijunyi.parallelgit.filesystem.io;
  
  import java.io.IOException;
  import java.util.*;
  import java.util.concurrent.ConcurrentHashMap;
  import javax.annotation.Nonnull;
  import javax.annotation.Nullable;
  
  import com.beijunyi.parallelgit.filesystem.GfsObjectService;
  import com.beijunyi.parallelgit.filesystem.exceptions.IncompatibleFileModeException;
  import com.beijunyi.parallelgit.utils.io.GitFileEntry;
  import com.beijunyi.parallelgit.utils.io.TreeSnapshot;
  import org.eclipse.jgit.lib.FileMode;
  import org.eclipse.jgit.lib.ObjectId;
  
  import static com.beijunyi.parallelgit.utils.io.GitFileEntry.*;
  import static java.util.Collections.*;
  import static org.eclipse.jgit.lib.FileMode.TREE;
  
  public class DirectoryNode extends Node<TreeSnapshot, Map<String, Node>> {
  
    protected DirectoryNode(ObjectId id, GfsObjectService objService) {
      super(id, TREE, objService);
    }
  
    protected DirectoryNode(GfsObjectService objService) {
      super(TREE, objService);
    }
  
    protected DirectoryNode(ObjectId id, DirectoryNode parent) {
      super(id, TREE, parent);
    }
  
    protected DirectoryNode(DirectoryNode parent) {
      super(TREE, parent);
    }
  
    @Nonnull
    public static DirectoryNode fromTree(ObjectId id, DirectoryNode parent) {
      return new DirectoryNode(id, parent);
    }
  
  
    @Nonnull
    public static DirectoryNode newDirectory(DirectoryNode parent) {
      return new DirectoryNode(parent);
    }
  
    @Override
    protected Class<? extends TreeSnapshot> getSnapshotType() {
      return TreeSnapshot.class;
    }
  
    @Override
    public long getSize() throws IOException {
      return 0;
    }
  
    @Override
    public void updateOrigin(GitFileEntry entry) throws IOException {
      super.updateOrigin(entry);
      if(isInitialized()) {
        if(origin.isSubtree()) {
          snapshot = objService.readTree(entry.getId());
          Set<String> updatedChildren = updateChildrenOrigins();
          Collection<Node> notUpdatedNodes = findNotUpdatedChildren(updatedChildren);
          updateOriginsToTrivial(notUpdatedNodes);
        } else {
          updateOriginsToTrivial(data.values());
        }
      }
    }
  
    public void updateOrigin(ObjectId id) throws IOException {
      updateOrigin(newTreeEntry(id));
    }
  
    @Nonnull
    @Override
    protected Map<String, Node> loadData(TreeSnapshot snapshot) throws IOException {
      Map<String, Node> ret = getDefaultData();
      boolean updateOrigin = origin != null && origin.getId().equals(snapshot.getId());
      for(Map.Entry<String, GitFileEntry> child : snapshot.getData().entrySet()) {
        GitFileEntry entry = child.getValue();
        Node node = Node.fromEntry(entry, this);
        ret.put(child.getKey(), node);
        if(updateOrigin)
          node.updateOrigin(entry);
      }
      return ret;
    }
  
    @Override
    protected boolean isTrivial(Map<String, Node> data) throws IOException {
      boolean ret = true;
      for(Node child : data.values())
        if(!child.isTrivial()) {
          ret = false;
          break;
        }
      return ret;
    }
  
    @Nonnull
    protected TreeSnapshot captureData(Map<String, Node> data, boolean persist) throws IOException {
      SortedMap<String, GitFileEntry> entries = new TreeMap<>();
      for(Map.Entry<String, Node> child : data.entrySet()) {
        Node node = child.getValue();
        ObjectId id = node.getObjectId(persist);
        if(!isTrivial(id))
          entries.put(child.getKey(), newEntry(id, node.getMode()));
      }
      return TreeSnapshot.capture(entries);
    }
  
    @Nonnull
    @Override
    public Node clone(DirectoryNode parent) throws IOException {
      DirectoryNode ret;
      if(isInitialized()) {
        ret = DirectoryNode.newDirectory(parent);
        for(Map.Entry<String, Node> child : data.entrySet()) {
          String name = child.getKey();
          Node node = child.getValue();
          ret.addChild(name, node.clone(ret), false);
        }
      } else if(id != null) {
        ret = DirectoryNode.fromTree(id, parent);
        parent.getObjectService().pullObject(id, objService);
      } else
        throw new IllegalStateException();
      return ret;
    }
  
    @Nonnull
    public List<String> listChildren() throws IOException {
      List<String> ret = new ArrayList<>(getData().keySet());
      Collections.sort(ret);
      return unmodifiableList(ret);
    }
  
    public boolean hasChild(String name) throws IOException {
      return getData().containsKey(name);
    }
  
    @Nullable
    public Node getChild(String name) throws IOException {
      return getData().get(name);
    }
  
    public boolean addChild(String name, Node child, boolean replace) throws IOException {
      if(!replace && getData().containsKey(name))
        return false;
      if(snapshot != null) {
        GitFileEntry origin = snapshot.getChild(name);
        if(!origin.isMissing()) child.updateOrigin(origin);
      }
      getData().put(name, child);
      id = null;
      invalidateParentCache();
      return true;
    }
  
    public boolean removeChild(String name) throws IOException {
      Node removed = getData().remove(name);
      if(removed != null) {
        removed.exile();
        id = null;
        invalidateParentCache();
        return true;
      }
      return false;
    }
  
    @Override
    protected void checkFileMode(FileMode proposed) {
      if(!TREE.equals(proposed))
        throw new IncompatibleFileModeException(TREE, proposed);
    }
  
    @Nonnull
    @Override
    protected Map<String, Node> getDefaultData() {
      return new ConcurrentHashMap<>();
    }
  
    @Nonnull
    private Set<String> updateChildrenOrigins() throws IOException {
      Set<String> ret = new HashSet<>();
      for(Map.Entry<String, GitFileEntry> child : snapshot.getData().entrySet()) {
        String name = child.getKey();
        Node node = data.get(name);
        if(node != null && !node.getOrigin().equals(child.getValue()))
          node.updateOrigin(child.getValue());
        ret.add(name);
      }
      return unmodifiableSet(ret);
    }
  
    @Nonnull
    private Collection<Node> findNotUpdatedChildren(Set<String> updatedChildren) throws IOException{
      List<Node> ret = new ArrayList<>();
      for(Map.Entry<String, Node> child : data.entrySet()) {
        String name = child.getKey();
        if(!updatedChildren.contains(name))
          ret.add(child.getValue());
      }
      return unmodifiableList(ret);
    }
  
    private void updateOriginsToTrivial(Collection<Node> nodes) throws IOException {
      for(Node node : nodes) {
        node.updateOrigin(missingEntry());
      }
    }
  
    @Override
    protected void exile() {
      super.exile();
      if(isInitialized()) {
        for(Node child : data.values())
          child.exile();
      }
    }
  
  }
  