code: package com.beijunyi.parallelgit.filesystem.io;
  
  import java.io.IOException;
  import javax.annotation.Nonnull;
  import javax.annotation.Nullable;
  
  import com.beijunyi.parallelgit.filesystem.GfsObjectService;
  import com.beijunyi.parallelgit.utils.io.GitFileEntry;
  import com.beijunyi.parallelgit.utils.io.ObjectSnapshot;
  import org.eclipse.jgit.lib.FileMode;
  import org.eclipse.jgit.lib.ObjectId;
  
  import static com.beijunyi.parallelgit.filesystem.io.DirectoryNode.fromTree;
  import static com.beijunyi.parallelgit.filesystem.io.FileNode.fromBlob;
  import static com.beijunyi.parallelgit.utils.io.GitFileEntry.missingEntry;
  import static org.eclipse.jgit.lib.FileMode.*;
  import static org.eclipse.jgit.lib.ObjectId.zeroId;
  
  public abstract class Node<Snapshot extends ObjectSnapshot, Data> {
  
    protected final GfsObjectService objService;
  
    protected volatile GitFileEntry origin = missingEntry();
    protected volatile DirectoryNode parent;
    protected volatile Snapshot snapshot;
    protected volatile ObjectId id;
    protected volatile FileMode mode;
    protected volatile Data data;
  
    protected Node(FileMode mode, GfsObjectService objService) {
      this.objService = objService;
      this.mode = mode;
      initialize();
    }
  
    protected Node(ObjectId id, FileMode mode, GfsObjectService objService) {
      this.objService = objService;
      this.id = id;
      this.mode = mode;
    }
  
    protected Node(FileMode mode, DirectoryNode parent) {
      this(mode, parent.getObjectService());
      this.parent = parent;
    }
  
    protected Node(ObjectId id, FileMode mode, DirectoryNode parent) {
      this(id, mode, parent.getObjectService());
      this.parent = parent;
    }
  
    protected Node(Data data, FileMode mode, DirectoryNode parent) {
      this(mode, parent);
      this.data = data;
    }
  
    @Nonnull
    public static Node fromEntry(GitFileEntry entry, DirectoryNode parent) {
      return TREE.equals(entry.getMode())
               ? fromTree(entry.getId(), parent)
               : fromBlob(entry.getId(), entry.getMode(), parent);
    }
  
    @Nonnull
    protected GfsObjectService getObjectService() {
      return objService;
    }
  
    public boolean isExecutableFile() {
      return EXECUTABLE_FILE.equals(getMode());
    }
  
    public boolean isRegularFile() {
      return REGULAR_FILE.equals(getMode()) || isExecutableFile();
    }
  
    public boolean isSymbolicLink() {
      return SYMLINK.equals(getMode());
    }
  
    public boolean isDirectory() {
      return TREE.equals(getMode());
    }
  
    @Nonnull
    public ObjectId getObjectId(boolean persist) throws IOException {
      if(id == null || persist && !objService.hasObject(id)) {
        Snapshot snapshot = takeSnapshot(persist);
        id = snapshot != null ? snapshot.getId() : zeroId();
      }
      return id;
    }
  
    @Nonnull
    public GitFileEntry getOrigin() {
      return origin;
    }
  
    public void updateOrigin(GitFileEntry entry) throws IOException {
      origin = entry;
    }
  
    @Nonnull
    public FileMode getMode() {
      return mode;
    }
  
    public void setMode(FileMode mode) {
      checkFileMode(mode);
      this.mode = mode;
      invalidateParentCache();
    }
  
    public boolean isNew() throws IOException {
      return origin.isMissing();
    }
  
    public boolean isModified() throws IOException {
      ObjectId id = getObjectId(false);
      return !origin.getId().equals(id) || !origin.getMode().equals(mode);
    }
  
    @Nonnull
    protected Data getData() throws IOException {
      if(data != null)
        return data;
      if(id == null) throw new IllegalStateException();
      data = loadData(loadSnapshot(id));
      return data;
    }
  
    protected boolean isTrivial() throws IOException {
      return isTrivial(getObjectId(false));
    }
  
    @Nonnull
    private Snapshot loadSnapshot(ObjectId id) throws IOException {
      Snapshot ret = objService.read(id, getSnapshotType());
      if(origin.getId().equals(id))
        snapshot = ret;
      return ret;
    }
  
    @Nullable
    protected Snapshot takeSnapshot(boolean persist) throws IOException {
      if(data == null) throw new IllegalStateException();
      if(isTrivial(data)) return null;
      Snapshot snapshot = captureData(data, persist);
      if(persist) objService.write(snapshot);
      return snapshot;
    }
  
    protected boolean isInitialized() {
      return data != null;
    }
  
    protected void initialize() {
      data = getDefaultData();
    }
  
    public void reset() {
      if(origin.isMissing()) throw new IllegalStateException();
      reset(origin);
    }
  
    protected void reset(GitFileEntry entry) {
      checkFileMode(mode);
      this.id = entry.getId();
      this.mode = entry.getMode();
      this.data = null;
      invalidateParentCache();
    }
  
    protected void invalidateParentCache() {
      if(parent != null) {
        parent.id = null;
        parent.invalidateParentCache();
      }
    }
  
    protected void exile() {
      parent = null;
    }
  
    protected abstract Class<? extends Snapshot> getSnapshotType();
  
    public abstract long getSize() throws IOException;
  
    protected abstract void checkFileMode(FileMode proposed);
  
    @Nonnull
    protected abstract Data getDefaultData();
  
    @Nonnull
    protected abstract Data loadData(Snapshot snapshot) throws IOException;
  
    protected abstract boolean isTrivial(Data data) throws IOException;
  
    @Nonnull
    protected abstract Snapshot captureData(Data data, boolean persist) throws IOException;
  
    @Nonnull
    protected abstract Node clone(DirectoryNode parent) throws IOException;
  
    protected static boolean isTrivial(ObjectId id) {
      return zeroId().equals(id);
    }
  
  }
  