gt:                         FileId fileId = item.getTask().getFile().getId();
context: package jdbox.uploader;
  
  import com.google.inject.Inject;
  import jdbox.OperationContext;
  import jdbox.models.File;
  import jdbox.models.fileids.FileId;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  import rx.Observer;
  
  import java.io.IOException;
  import java.io.PrintWriter;
  import java.io.StringWriter;
  import java.util.*;
  import java.util.concurrent.*;
  import java.util.concurrent.locks.ReadWriteLock;
  import java.util.concurrent.locks.ReentrantReadWriteLock;
  
  public class Uploader {
  
      public static final String uploadFailureNotificationFileId = "upload failure notification file id";
  
      private static final Logger logger = LoggerFactory.getLogger(Uploader.class);
  
      private final Observer<UploadFailureEvent> uploadFailureEvent;
      private final Observer<FileEtagUpdateEvent> fileEtagUpdateEvent;
      private final Map<FileId, Queue> queues = new HashMap<>();
      private final List<Future> futures = new LinkedList<>();
  
      private final ReadWriteLock remoteStateLock = new ReentrantReadWriteLock(true);
  
      private volatile UploadStatus uploadStatus;
      private volatile ExecutorService executor;
  
      @Inject
      public Uploader(
              Observer<UploadFailureEvent> uploadFailureEvent, Observer<FileEtagUpdateEvent> fileEtagUpdateEvent) {
          this.uploadFailureEvent = uploadFailureEvent;
          this.fileEtagUpdateEvent = fileEtagUpdateEvent;
      }
  
      public UploadStatus getCurrentStatus() {
          return uploadStatus;
      }
  
      public synchronized int getQueueCount() {
          return queues.size();
      }
  
      public synchronized boolean fileIsQueued(FileId fileId) {
          return queues.get(fileId) != null;
      }
  
      public void init() {
          executor = new ThreadPoolExecutor(4, 4, 60, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>());
      }
  
      public void tearDown() throws InterruptedException {
  
          if (executor != null) {
              executor.shutdown();
              executor.awaitTermination(5, TimeUnit.SECONDS);
              executor = null;
          }
      }
  
      public synchronized void reset() {
          uploadStatus = null;
          queues.clear();
          futures.clear();
      }
  
      /**
       * TODO Strictly speaking, when a task to delete a directory is submitted, this task must depend on ALL tasks
       * related to files in this directory. However, since deletion just moves files/directories to trash, this will
       * work. Still, I should find a way to fix this.
       */
      public synchronized void submit(Task task) {
  
          logger.debug("submitting {}", task);
  
          File file = task.getFile();
          FileId fileId = file.getId();
          String etag = file.getEtag();
  
          Queue queue = queues.get(fileId);
  
          if (queue == null) {
              queue = new Queue(etag);
              queues.put(fileId, queue);
          }
  
          final Item item = queue.append(task, OperationContext.get());
  
          if (task.getDependsOn() != null) {
              Queue dependency = queues.get(task.getDependsOn());
              if (dependency != null && dependency.getHead().getTask().blocksDependentTasks())
                  item.addDependency(dependency.getHead());
          }
  
          if (item.getDependencies().size() == 0)
              trySubmitToExecutor(item);
  
          if (isBroken())
              updateStatus(uploadStatus.exception);
      }
  
      public void waitUntilIsDone(long period, TimeUnit units) throws InterruptedException, TimeoutException {
          waitUntilIsDone(false, period, units);
      }
  
      public void waitUntilIsDoneOrBroken(long period, TimeUnit units) throws InterruptedException, TimeoutException {
          waitUntilIsDone(true, period, units);
      }
  
      public void pause() {
          remoteStateLock.writeLock().lock();
      }
  
      public boolean tryPause(int timeout, TimeUnit unit) {
  
          boolean locked = false;
  
          try {
              locked = remoteStateLock.writeLock().tryLock(timeout, unit);
          } catch (InterruptedException e) {
              logger.debug("lock acquisition has been interrupted", e);
          }
  
          return locked;
      }
  
      public void resume() {
          remoteStateLock.writeLock().unlock();
      }
  
      private boolean isBroken() {
          return uploadStatus != null;
      }
  
      private void updateStatus(Exception e) {
          uploadStatus = new UploadStatus(e);
          uploadFailureEvent.onNext(new UploadFailureEvent(uploadStatus));
      }
  
      private void trySubmitToExecutor(Item item) {
  
          if (isBroken())
              return;
  
          logger.debug("submitting to executor {}", item.getTask());
          futures.add(executor.submit(new TaskRunner(item)));
      }
  
      private void waitUntilIsDone(
              boolean canBeBroken, long period, TimeUnit units) throws InterruptedException, TimeoutException {
  
          if (futures.size() == 0)
              return;
  
          Date start = new Date();
  
          List<Future> futures;
  
          do {
  
              synchronized (this) {
  
                  if (!canBeBroken && isBroken())
                      throw new AssertionError("uploader is broken even though it is not allowed");
  
                  futures = new LinkedList<>(this.futures);
                  this.futures.clear();
              }
  
              for (Future future : futures) {
                  long timeToWait =
                          TimeUnit.MILLISECONDS.convert(period, units) - (new Date().getTime() - start.getTime());
                  try {
                      future.get(timeToWait, TimeUnit.MILLISECONDS);
                  } catch (ExecutionException e) {
                      throw new AssertionError("an unexpected error occurred while waiting for a task to finish", e);
                  }
              }
  
          } while (futures.size() > 0);
      }
  
      public class UploadStatus {
  
          public final Date date = new Date();
  
          private final Exception exception;
  
          private volatile String serialized;
  
          private UploadStatus(Exception exception) {
              this.exception = exception;
          }
  
          public String asString() {
  
              if (serialized == null) {
                  synchronized (Uploader.this) {
                      if (serialized == null)
                          serialized = serialize();
                  }
              }
  
              return serialized;
          }
  
          private String serialize() {
  
              StringBuilder sb = new StringBuilder();
  
              //                                                                                        | 79 chars
              sb.append("Upload is broken due to an error occured while uploading changes to the cloud.\n\n");
              sb.append("Back up all the data that you recently added to or modified in JdBox and then\n");
              sb.append("delete this file to discard the local state and start from the state in the\n");
              sb.append("cloud. Until you remove this file, all local modifications will be accumulated,\n");
              sb.append("to allow you to back up your data safely, but JdBox will never attempt to\n");
              sb.append("upload these changes to the cloud.\n\n");
  
              sb.append("The following changes have been accumulated:\n\n");
  
              for (Queue queue : queues.values()) {
                  Item item = queue.getHead();
                  do {
                      sb.append("    ").append(item.getTask().getLabel()).append("\n");
                  } while ((item = item.getNext()) != null);
                  sb.append("\n");
              }
  
              sb.append("The error that originally occured:\n\n    ");
  
              StringWriter sw = new StringWriter();
              PrintWriter pw = new PrintWriter(sw);
              exception.printStackTrace(pw);
              sb.append(sw.toString()).append("\n");
  
              return sb.toString();
          }
      }
  
      private class TaskRunner implements Runnable {
  
          private final Item item;
  
          private TaskRunner(Item item) {
              this.item = item;
          }
  
          @Override
          public void run() {
  
              int delay = 1;
              Random random = new Random();
  
              String etag = null;
              Queue queue = item.getQueue();
  
              while (etag == null) {
  
                  try {
  
                      remoteStateLock.readLock().lock();
  
                      try {
  
                          OperationContext.restore(item.getCtx());
  
                          try {
  
                              logger.debug("starting {} with etag {}", item.getTask(), queue.getEtag());
  
                              etag = item.getTask().run(queue.getEtag());
  
                              logger.debug("completed {}, new etag is {}", item.getTask(), etag);
  
                          } finally {
                              OperationContext.clear();
                          }
  
  