private void recalcHashes(DataNode item) throws IOException {
   if (item.dirty == null) {
     return; // not dirty, which means no children are dirty
   }
   // only directories have derived hashes
   if (item instanceof DirectoryNode) {
     DirectoryNode dirNode = (DirectoryNode) item;
     for (DataNode child : dirNode) {
       recalcHashes(child);
     }
     ByteArrayOutputStream bout = new ByteArrayOutputStream();
     hashCalc.sort(dirNode.getChildren());
     String newHash = hashCalc.calcHash(dirNode, bout);
     item.setHash(newHash);
     byte[] arrTriplets = bout.toByteArray();
     blobStore.setBlob(newHash, arrTriplets);
     log.info(
         "recalcHashes: "
             + item.name
             + " children:"
             + dirNode.members.size()
             + " hash="
             + newHash);
   }
 }
 public List<ITriplet> find(String hash) {
   // return DataItem.findByHash(hash, session);
   byte[] arr = blobStore.getBlob(hash);
   if (arr == null) {
     return null;
   }
   ByteArrayInputStream bin = new ByteArrayInputStream(arr);
   try {
     return hashCalc.parseTriplets(bin);
   } catch (IOException ex) {
     throw new RuntimeException(ex);
   }
 }
/**
 * A DataSession provides a simple way to read and write the versioned content repository
 *
 * <p>Simply locate nodes, move them and modify them as you would expect through the API, then when
 * the session is saved the complete state of the repository is updated
 *
 * @author brad
 */
public class DataSession {

  private static final Logger log = LoggerFactory.getLogger(DataSession.class);
  private DirectoryNode rootDataNode;
  private final Session session;
  private final HashStore hashStore;
  private final BlobStore blobStore;
  private final HashCalc hashCalc = HashCalc.getInstance();
  private final CurrentDateService currentDateService;
  private final Branch branch;

  public DataSession(
      Branch branch,
      Session session,
      HashStore hashStore,
      BlobStore blobStore,
      CurrentDateService currentDateService) {
    this.blobStore = blobStore;
    this.hashStore = hashStore;
    this.session = session;
    this.branch = branch;
    this.currentDateService = currentDateService;
    Commit c = branch.latestVersion(session);
    String hash = null;
    if (c != null) {
      hash = c.getItemHash();
    }
    rootDataNode = new DirectoryNode(null, null, hash);
  }

  public DirectoryNode getRootDataNode() {
    return rootDataNode;
  }

  public DataNode find(Path path) {
    if (path.isRoot()) {
      return rootDataNode;
    } else {
      DataNode parent = find(path.getParent());
      if (parent == null) {
        return null;
      } else if (parent instanceof DirectoryNode) {
        DirectoryNode dirNode = (DirectoryNode) parent;
        return dirNode.get(path.getName());
      } else {
        return null;
      }
    }
  }

  public boolean dirExists(String hash) {
    return blobStore.hasBlob(hash);
  }

  public List<ITriplet> find(String hash) {
    // return DataItem.findByHash(hash, session);
    byte[] arr = blobStore.getBlob(hash);
    if (arr == null) {
      return null;
    }
    ByteArrayInputStream bin = new ByteArrayInputStream(arr);
    try {
      return hashCalc.parseTriplets(bin);
    } catch (IOException ex) {
      throw new RuntimeException(ex);
    }
  }

  /**
   * Saves any changed items in the session by recalculating hashes and persisting new items to the
   * repository, and returns a list of root tree items which represent changed trees. That list will
   * contain the root of the tree which this session directly represents, but it will also contain
   * the roots of trees which have been affected through linked item updates
   *
   * @return
   */
  public String save(Profile currentUser) throws IOException {
    recalcHashes(rootDataNode);

    String newHash = rootDataNode.hash;

    Commit newCommit = new Commit();
    newCommit.setCreatedDate(currentDateService.getNow());
    newCommit.setEditor(currentUser);
    newCommit.setItemHash(newHash);
    session.save(newCommit);
    System.out.println("commit hash: " + newHash + " id " + newCommit.getId());
    branch.setHead(newCommit);
    session.save(branch);

    return rootDataNode.hash;
  }

  private void recalcHashes(DataNode item) throws IOException {
    if (item.dirty == null) {
      return; // not dirty, which means no children are dirty
    }
    // only directories have derived hashes
    if (item instanceof DirectoryNode) {
      DirectoryNode dirNode = (DirectoryNode) item;
      for (DataNode child : dirNode) {
        recalcHashes(child);
      }
      ByteArrayOutputStream bout = new ByteArrayOutputStream();
      hashCalc.sort(dirNode.getChildren());
      String newHash = hashCalc.calcHash(dirNode, bout);
      item.setHash(newHash);
      byte[] arrTriplets = bout.toByteArray();
      blobStore.setBlob(newHash, arrTriplets);
      log.info(
          "recalcHashes: "
              + item.name
              + " children:"
              + dirNode.members.size()
              + " hash="
              + newHash);
    }
  }

  /**
   * Represents a logical view of a member in the versioned content repository.
   *
   * <p>An item implements Iterable so its children can be iterated over, and mutating operations
   * must be performed through the DataNode methods.
   */
  public abstract class DataNode implements ITriplet {

    protected DirectoryNode parent;
    protected String name;
    protected String type;
    protected String hash;
    protected String loadedHash; // holds the hash value from when the node was loaded
    protected Boolean dirty;

    /**
     * Copy just creates the same type of item with the same hash
     *
     * @param newDir
     * @param newName
     */
    public abstract void copy(DirectoryNode newDir, String newName);

    private DataNode(DirectoryNode parent, String name, String type, String hash) {
      this.parent = parent;
      this.name = name;
      this.type = type;
      this.hash = hash;
      this.loadedHash = hash;
    }

    /**
     * Move this item to a new parent, or to a new name, or both
     *
     * @param newParent
     */
    public void move(DirectoryNode newParent, String newName) {
      DirectoryNode oldParent = this.getParent();
      if (oldParent != newParent) {
        this.setParent(newParent);
        if (oldParent.members != null) {
          oldParent.members.remove(this);
        }
        newParent.getChildren().add(this);
        setDirty();
        newParent.setDirty();
        oldParent.setDirty();
      }
      if (!newName.equals(name)) {
        setName(newName);
      }
      parent.checkConsistency(this);
    }

    public void delete() {
      parent.getChildren().remove(this);
      parent.checkConsistency(parent);
      setDirty();
    }

    public DirectoryNode getParent() {
      return parent;
    }

    private void setParent(DirectoryNode parent) {
      this.parent = parent;
    }

    @Override
    public String getName() {
      return name;
    }

    private void setName(String name) {
      this.name = name;
      setDirty();
    }

    @Override
    public String getType() {
      return type;
    }

    public void setType(String type) {
      this.type = type;
    }

    @Override
    public String getHash() {
      return hash;
    }

    public void setHash(String hash) {
      log.info("setHash: " + hash + " on " + getName());
      this.hash = hash;
      setDirty();
    }

    public String getLoadedHash() {
      return loadedHash;
    }

    protected void setDirty() {
      if (dirty != null) {
        return; // already set
      }
      dirty = Boolean.TRUE;
      if (parent != null) {
        parent.setDirty();
      }
    }
  }

  public class DirectoryNode extends DataNode implements Iterable<DataNode> {

    private List<DataNode> members;

    public DirectoryNode(DirectoryNode parent, String name, String hash) {
      super(parent, name, "d", hash);
    }

    @Override
    public void copy(DirectoryNode newDir, String newName) {
      newDir.addDirectory(newName, this.hash);
    }

    private List<DataNode> getChildren() {
      if (members == null) {
        members = new ArrayList<>();
        if (hash != null) {
          List<ITriplet> list = find(hash);
          if (list != null) {
            for (ITriplet i : list) {
              DataNode c;
              if (i.getType().equals("d")) {
                c = new DirectoryNode(this, i.getName(), i.getHash());
              } else {
                c = new FileNode(this, i.getName(), i.getHash());
              }
              members.add(c);
            }
          }
        }
        // log.info("DirectoryNode: loaded children for " + getName() + " = " + members.size() + "
        // from hash: " + hash);
      }
      return members;
    }

    public FileNode addFile(String name) {
      return addFile(name, null);
    }

    public FileNode addFile(String name, String hash) {
      log.info("addFile: " + name + " - " + hash);
      FileNode item = new FileNode(this, name, hash);
      getChildren().add(item);
      checkConsistency(item);
      setDirty();
      return item;
    }

    public DirectoryNode addDirectory(String name, String hash) {
      DirectoryNode item = new DirectoryNode(this, name, hash);
      getChildren().add(item);
      setDirty();
      return item;
    }

    public DirectoryNode addDirectory(String name) {
      return addDirectory(name, null);
    }

    public int size() {
      return getChildren().size();
    }

    public boolean isEmpty() {
      return getChildren().isEmpty();
    }

    public boolean contains(DataNode o) {
      return getChildren().contains(o);
    }

    @Override
    public Iterator<DataNode> iterator() {
      return getChildren().iterator();
    }

    public DataNode get(String name) {
      for (DataNode i : getChildren()) {
        if (i.getName().equals(name)) {
          return i;
        }
      }
      return null;
    }

    /**
     * called after modifying the list. It checks that names within the list are unique, and that
     * every item has this item as its parent
     *
     * @param newItem
     */
    private void checkConsistency(DataNode newItem) {
      if (members == null) {
        return; // nothing changed
      }
      Set<String> names = new HashSet<>();
      for (DataNode item : members) {
        if (names.contains(item.getName())) {
          if (newItem != null) {
            throw new RuntimeException(
                "Found duplicate name: "
                    + item.getName()
                    + " when adding item: "
                    + newItem.getName()
                    + " to directory: "
                    + getName());
          } else {
            throw new RuntimeException(
                "Found duplicate name: " + item.getName() + " in parent: " + getName());
          }
        }
        if (item.getParent() != this) {
          throw new RuntimeException(
              "Found item in this set which does not have this item as its parent: "
                  + item.getName()
                  + ". Its parent is: "
                  + item.getParent().getName()
                  + " and my name is : "
                  + this.getName());
        }
        names.add(item.getName());
      }
    }
  }

  public class FileNode extends DataNode {

    private Fanout fanout;

    public FileNode(DirectoryNode parent, String name, String hash) {
      super(parent, name, "f", hash);
    }

    @Override
    public void copy(DirectoryNode newDir, String newName) {
      newDir.addFile(newName, this.hash);
    }

    public void setContent(InputStream in) throws IOException {
      Parser parser = new Parser();
      String fileHash = parser.parse(in, hashStore, blobStore);
      setHash(fileHash);
    }

    private Fanout getFanout() {
      if (fanout == null) {
        fanout = hashStore.getFileFanout(getHash());
        if (fanout == null) {
          throw new RuntimeException("Fanout not found: " + getHash());
        }
      }
      return fanout;
    }

    public void writeContent(OutputStream out) throws IOException {
      Combiner combiner = new Combiner();
      List<String> fanoutCrcs = getFanout().getHashes();
      combiner.combine(fanoutCrcs, hashStore, blobStore, out);
      out.flush();
    }

    /**
     * Write partial content, only
     *
     * @param out
     * @param start
     * @param finish
     * @throws IOException
     */
    public void writeContent(OutputStream out, long start, Long finish) throws IOException {
      Combiner combiner = new Combiner();
      List<String> fanoutCrcs = getFanout().getHashes();
      combiner.combine(fanoutCrcs, hashStore, blobStore, out, start, finish);
      out.flush();
    }

    public long getContentLength() {
      return getFanout().getActualContentLength();
    }
  }
}