@Override
  public void onResponse(IResponse response) {
    if (!(response instanceof FilePushResponse)) {
      logger.error(
          "Expected response to be instance of "
              + FilePushResponse.class.getName()
              + " but got "
              + response.getClass().getName());
      return;
    }

    if (StatusCode.ERROR.equals(((FilePushResponse) response).getStatusCode())
        || 0 > ((FilePushResponse) response).getChunkCounter()) {
      // exchange is finished or an error occurred on the other side
      super.node
          .getObjectDataReplyHandler()
          .removeResponseCallbackHandler(response.getExchangeId());
      super.onResponse(response);
      this.chunkCountDownLatch.countDown();
    } else {
      this.sendChunk(
          ((FilePushResponse) response).getChunkCounter(),
          this.fileId,
          this.owner,
          response.getExchangeId(),
          new NodeLocation(
              response.getClientDevice().getUserName(),
              response.getClientDevice().getClientDeviceId(),
              response.getClientDevice().getPeerAddress()));
    }
  }
  @Override
  public void await(long timeout, TimeUnit timeUnit) throws InterruptedException {
    // only wait for parent if we actually have sent a request
    if (this.clientCounter > 0) {
      super.await(timeout, timeUnit);
    }

    // wait for receivers to be initialised
    this.initReceiverLatch.await(timeout, timeUnit);

    this.chunkCountDownLatch.await(timeout, timeUnit);
  }
  @Override
  public void await() throws InterruptedException {
    // only wait for parent if we actually have sent a request
    if (this.clientCounter > 0) {
      super.await();
    }

    // wait for receivers to be initialised
    this.initReceiverLatch.await();

    this.chunkCountDownLatch.await(MAX_FILE_WAITNG_TIME, TimeUnit.MILLISECONDS);
  }
  /**
   * Send a chunk to another client
   *
   * @param chunkCounter The chunk counter
   * @param exchangeId The exchange id for the request
   * @param receiver The receiver which should get the chunk
   */
  protected void sendChunk(
      long chunkCounter, UUID fileId, String owner, UUID exchangeId, NodeLocation receiver) {
    Chunk chunk = new Chunk("", "", new HashSet<>(), true, AccessType.WRITE, -1, -1, -1, null);

    try {
      chunk = this.chunkProvider.getChunk(chunkCounter, CHUNK_SIZE);
    } catch (InputOutputException e) {
      logger.error(
          "Failed to read the chunk "
              + chunkCounter
              + " of file "
              + this.relativeFilePath
              + " for exchange "
              + this.exchangeId
              + ". Aborting file push exchange. Message: "
              + e.getMessage());
      return;
    } catch (IllegalArgumentException e) {
      // requested chunk does not exist anymore
      logger.info(
          "Detected file change during push exchange "
              + this.exchangeId
              + ". Starting to push again at chunk 0");
      try {
        chunk = this.chunkProvider.getChunk(0, CHUNK_SIZE);
      } catch (InputOutputException e1) {
        logger.error(
            "Failed to read the chunk "
                + chunkCounter
                + " of file "
                + this.relativeFilePath
                + " for exchange "
                + this.exchangeId
                + " after detected file change. Aborting file push exchange. Message: "
                + e.getMessage());
      }
    }

    // check whether the chunk counter has changed
    StatusCode statusCode =
        (chunkCounter == chunk.getChunkCounter()) ? StatusCode.NONE : StatusCode.FILE_CHANGED;

    IRequest request =
        new FilePushRequest(
            exchangeId,
            statusCode,
            this.clientDevice,
            chunk.getChecksum(),
            fileId,
            owner,
            chunk.getAccessType(),
            chunk.getSharers(),
            this.relativeFilePath,
            chunk.isFile(),
            chunk.getChunkCounter(),
            CHUNK_SIZE,
            chunk.getTotalNrOfChunks(),
            chunk.getTotalFileSize(),
            chunk.getData(),
            receiver);

    logger.info(
        "Sending chunk "
            + chunkCounter
            + " to client "
            + receiver.getPeerAddress().inetAddress().getHostAddress()
            + ":"
            + receiver.getPeerAddress().tcpPort());

    super.sendRequest(request);
  }
  @Override
  public void run() {
    try {
      // check whether the own client is also in the list (should be usually, but you never know...)
      this.clientCounter = this.receivers.size();
      for (NodeLocation location : this.receivers) {
        if (location.getPeerAddress().equals(this.node.getPeerAddress())) {
          this.clientCounter--;
          break;
        }
      }

      this.chunkCountDownLatch = new CountDownLatch(this.clientCounter);
      this.initReceiverLatch.countDown();

      // check whether we got access to the file
      this.fileId = null;
      this.owner = null;
      try {
        PathObject pathObject =
            this.objectStore.getObjectManager().getObjectForPath(this.relativeFilePath);
        // if we are not the owner but have access to the file
        if (null != pathObject.getOwner()
            && !this.node.getUser().getUserName().equals(pathObject.getOwner())
            && AccessType.WRITE.equals(pathObject.getAccessType())) {
          try {
            this.fileId = this.node.getIdentifierManager().getValue(this.relativeFilePath);
            this.owner = pathObject.getOwner();
          } catch (InputOutputException e) {
            logger.error(
                "Failed to get file id for "
                    + this.relativeFilePath
                    + ". Message: "
                    + e.getMessage());
          }
        }

        // add file id also if the path is shared
        if (pathObject.isShared()) {
          for (Sharer entry : pathObject.getSharers()) {
            try {
              // ask sharer's clients to get the changes too
              List<NodeLocation> sharerLocations =
                  this.nodeManager.getNodeLocations(entry.getUsername());

              // only add one client of the sharer. He may propagate the change then
              // to his clients, and if a conflict occurs, there will be a new file
              if (!sharerLocations.isEmpty()) {
                fileId = super.node.getIdentifierManager().getValue(pathObject.getAbsolutePath());
                // Note that we do not add the sharer location again since these
                // are assembled in FileOfferExchangeHandlerResult
              }
            } catch (InputOutputException e) {
              logger.error(
                  "Could not get client locations of sharer "
                      + entry.getUsername()
                      + ". This sharer's clients do not get the file (change)");
            }
          }
        }
      } catch (InputOutputException e) {
        logger.error(
            "Failed to read path object for "
                + this.relativeFilePath
                + ". Message: "
                + e.getMessage());
      }

      // the owner of a file is only added on a share request
      for (NodeLocation location : this.receivers) {
        UUID uuid = UUID.randomUUID();
        logger.info(
            "Sending first chunk as subRequest of "
                + this.exchangeId
                + " with id "
                + uuid
                + " to client "
                + location.getPeerAddress().inetAddress().getHostName()
                + ":"
                + location.getPeerAddress().tcpPort());
        // add callback handler for sub request
        super.node.getObjectDataReplyHandler().addResponseCallbackHandler(uuid, this);

        this.sendChunk(
            0, // first chunk
            this.fileId,
            this.owner,
            uuid,
            location);
      }
    } catch (Exception e) {
      logger.error("Failed to execute FilePushExchangeHandler. Message: " + e.getMessage(), e);
    }
  }