/**
  * INTERNAL: Get the appropriate attribute value from the object and put it in the appropriate
  * field of the database row. Loop through the reference objects and extract the primary keys and
  * put them in the vector of "nested" rows.
  */
 public void writeFromObjectIntoRowWithChangeRecord(
     ChangeRecord changeRecord, AbstractRecord row, AbstractSession session) {
   if (isForeignKeyRelationship()) {
     Object object = ((ObjectChangeSet) changeRecord.getOwner()).getUnitOfWorkClone();
     this.writeFromObjectIntoRow(object, row, session);
   } else {
     super.writeFromObjectIntoRowWithChangeRecord(changeRecord, row, session);
   }
 }
  private void synchronize(ChangeRecord record) throws IOException {
    try {
      switch (record.getOperation()) {
        case LOCAL_INSERT:
          {
            // Get remote id for the parent folder
            File local = DriveUtils.absolutePath(record.getLocalFile());
            String remoteParent =
                stub.storage().localToRemote().get(DriveUtils.relativePath(local.getParentFile()));
            if (!StringUtils.isEmpty(remoteParent)
                && !stub.storage().localToRemote().containsKey(record.getLocalFile())) {
              // Ignore insert request that doesn't have a parent or
              // already have a mapping.
              // The insert operation will be accomplished by the topmost
              // folder
              String remoteId = stub.transmit().upload(remoteParent, local);
              record.setRemoteFileId(remoteId);
            }
            break;
          }
        case LOCAL_DELETE:
          {
            if (!StringUtils.isEmpty(record.getRemoteFileId())) {
              stub.transmit().delete(record.getRemoteFileId());
            } else {
              logger.warn(
                  "Local deletion has no remote reference, make sure the storage is correct.");
            }
            break;
          }
        case LOCAL_CHANGE:
          {
            if (!StringUtils.isEmpty(record.getRemoteFileId())) {
              File local = DriveUtils.absolutePath(record.getLocalFile());
              stub.transmit().update(record.getRemoteFileId(), local);
            } else {
              logger.warn(
                  "Local change has no remote reference, make sure the storage is correct.");
              logger.warn("Trying to insert the new record");
              // Modify it to a local insert
              record.setOperation(Operation.LOCAL_INSERT);
              synchronize(record);
            }
            break;
          }
        case LOCAL_RENAME:
          {
            if (!StringUtils.isEmpty(record.getRemoteFileId())) {
              stub.transmit().rename(record.getRemoteFileId(), (String) record.getContext()[0]);
            } else {
              logger.warn(
                  "Local rename has no remote reference, make sure the storage is correct.");
              logger.warn("Trying to insert the new record");
              // Modify it to be a local insert
              record.setOperation(Operation.LOCAL_INSERT);
              synchronize(record);
            }
            break;
          }
        case REMOTE_INSERT:
          {
            if (stub.storage().remoteToLocal().containsKey(record.getRemoteFileId())) {
              // This file/folder already had been created
              break;
            }
            com.google.api.services.drive.model.File file = record.getContext(0);

            // Depth search of parent that has a local root
            List<ParentReference> path = new ArrayList<ParentReference>();
            File local = pathToLocal(record.getRemoteFileId(), path);
            if (null == local) {
              // Check whether this file is trashed
              if (file.getLabels().getTrashed()) {
                return;
              }
              if (file.getShared()) {
                return;
              } else {
                throw new IllegalArgumentException("Non-trash file has no known parent");
              }
            }
            File parent = local;
            for (ParentReference node : path) {
              stub.transmit().download(node.getId(), parent);
              parent = DriveUtils.absolutePath(stub.storage().remoteToLocal().get(node.getId()));
            }
            stub.transmit().download(record.getRemoteFileId(), parent);
            record.setLocalFile(stub.storage().remoteToLocal().get(record.getRemoteFileId()));
            if (StringUtils.isEmpty(record.getLocalFile())) {
              break;
            }
            break;
          }
        case REMOTE_DELETE:
          {
            if (StringUtils.isEmpty(record.getLocalFile())) {
              // No local file, remote file should be a trashed one.
              if (logger.isDebugEnabled()) {
                logger.debug(
                    "No corresponding local file for deletion. Remote file may be trashed");
              }
              break;
            }
            File localFile = DriveUtils.absolutePath(record.getLocalFile());
            String localParent = DriveUtils.relativePath(localFile.getParentFile());
            // Preserve the original context
            record.setContext(new Object[] {record.getContext(0), localParent});
            deleteLocalFile(localFile);
            if (localFile.exists()) {
              if (logger.isDebugEnabled()) {
                logger.debug(
                    MessageFormat.format(
                        "Failed to delete file {0}. File may have been deleted.",
                        record.getLocalFile()));
              }
            }
            stub.storage().localToRemote().remove(record.getLocalFile());
            stub.storage().remoteToLocal().remove(record.getRemoteFileId());
            break;
          }
        case REMOTE_CHANGE:
          {
            File local = DriveUtils.absolutePath(record.getLocalFile());
            File localParent = local.getParentFile();
            com.google.api.services.drive.model.File remote = record.getContext(0);

            if (!remote.getTitle().equals(local.getName())) {
              local.delete();
              stub.storage().localToRemote().remove(record.getLocalFile());
              stub.storage().remoteToLocal().remove(record.getRemoteFileId());
            }
            stub.transmit().download(record.getRemoteFileId(), localParent);
            break;
          }
        case REMOTE_RENAME:
          {
            File local = DriveUtils.absolutePath(record.getLocalFile());
            com.google.api.services.drive.model.File remote = record.getContext(0);
            String remoteName = remote.getTitle();
            File newName =
                new File(local.getParentFile().getAbsolutePath() + File.separator + remoteName);
            String relNewName = DriveUtils.relativePath(newName);
            record.setContext(new Object[] {record.getContext()[0], relNewName});
            local.renameTo(newName);
            stub.storage()
                .remoteToLocal()
                .put(record.getRemoteFileId(), DriveUtils.relativePath(newName));
            stub.storage().localToRemote().remove(DriveUtils.relativePath(local));
            stub.storage()
                .localToRemote()
                .put(DriveUtils.relativePath(newName), record.getRemoteFileId());
            break;
          }
      }
    } catch (Exception e) {
      logger.warn(MessageFormat.format("Exception on Change {0}. Retry later", record), e);
      FailedRecord fr = new FailedRecord(record);
      if (e instanceof GoogleJsonResponseException) {
        GoogleJsonResponseException gjre = (GoogleJsonResponseException) e;
        fr.setError(String.valueOf(gjre.getDetails().getCode()));
      } else {
        fr.setError(e.getMessage());
      }
      stub.storage().failedLog().add(fr);
    }
  }
  @Override
  public void synchronize() throws IOException {
    if (logger.isDebugEnabled()) {
      logger.debug("Synchronizing...");
    }
    // Detect remote change
    List<ChangeRecord> remoteChanges = remoteChange();
    // Detect local change
    Snapshot standard = stub.storage().get(StorageService.SNAPSHOT);
    Snapshot current = stub.snapshot().make();
    List<ChangeRecord> localChanges = compare(standard, current);

    /*
     * First upload then download. Local change has higher priority than
     * remote because remote change can be restored while local cannot.
     * Recorded uploaded file so later no need to download
     */

    Map<String, String> localUploaded = new HashMap<String, String>();
    for (ChangeRecord localChange : localChanges) {
      synchronize(localChange);
      localUploaded.put(localChange.getLocalFile(), localChange.getRemoteFileId());
    }

    PriorityQueue<ChangeRecord> orderedRemoteChanges =
        new PriorityQueue<ChangeRecord>(
            50,
            new Comparator<ChangeRecord>() {
              @Override
              public int compare(ChangeRecord o1, ChangeRecord o2) {
                if (o1.getOperation().ordinal() < o2.getOperation().ordinal()) {
                  return -1;
                } else if (o1.getOperation().ordinal() > o2.getOperation().ordinal()) {
                  return 1;
                } else {
                  if (o1.getLocalFile() == null) return -1;
                  if (o2.getLocalFile() == null) return 1;
                  return o1.getLocalFile().compareTo(o2.getLocalFile());
                }
              }
            });

    for (ChangeRecord remoteChange : remoteChanges) {
      if (!localUploaded.containsKey(remoteChange.getLocalFile())) {
        synchronize(remoteChange);
        if (remoteChange.getOperation() == Operation.REMOTE_INSERT) {
          remoteChange.setLocalFile(
              stub.storage().remoteToLocal().get(remoteChange.getRemoteFileId()));
        }
        orderedRemoteChanges.add(remoteChange);
      }
    }

    while (!orderedRemoteChanges.isEmpty()) {
      addToSnapshot(current, orderedRemoteChanges.poll());
    }

    // Retry until all the errors are processed
    while (!stub.storage().failedLog().isEmpty()) {
      FailedRecord fr = stub.storage().failedLog().remove(0);
      // Filter, modify and discard
      fr = correct(fr);
      if (fr != null) {
        synchronize(fr);
      }
    }

    stub.storage().put(StorageService.SNAPSHOT, current);
  }
  private void addToSnapshot(Snapshot root, ChangeRecord remoteChange) {
    switch (remoteChange.getOperation()) {
      case REMOTE_INSERT:
        String localName = stub.storage().remoteToLocal().get(remoteChange.getRemoteFileId());
        if (StringUtils.isEmpty(localName)) {
          // Insert failed
          return;
        }
        File localFile = DriveUtils.absolutePath(localName);
        String parent = DriveUtils.relativePath(localFile.getParentFile());
        if (root.getName().equals(parent)) {
          for (Snapshot sn : root.getChildren()) {
            if (sn.getName().equals(localName)) return;
          }
          Snapshot sn = stub.snapshot().make(localFile);
          root.addChild(sn);
        } else {
          for (Snapshot sn : root.getChildren()) {
            if (parent.startsWith(sn.getName())) {
              addToSnapshot(sn, remoteChange);
              return;
            }
          }
          // TODO the operations are not in sequence.
          // Didn't find?
          logger.warn(
              MessageFormat.format("Remote insert cannot find local parent {0}", remoteChange));
          // throw new IllegalArgumentException();
        }
        break;
      case REMOTE_CHANGE:
        {
          String fileName = remoteChange.getLocalFile();
          if (fileName.equals(root.getName())) {
            com.google.api.services.drive.model.File remoteFile = remoteChange.getContext(0);
            root.setMd5Checksum(remoteFile.getMd5Checksum());
          } else {
            for (Snapshot sn : root.getChildren()) {
              if (fileName.startsWith(sn.getName())) {
                addToSnapshot(sn, remoteChange);
                return;
              }
            }
            // Didn't find?
            logger.error(
                MessageFormat.format(
                    "Local root doesn't contain this file {0}. Should be an error.", remoteChange));
            // throw new IllegalArgumentException();
          }
          break;
        }
      case REMOTE_RENAME:
        {
          String fileName = remoteChange.getLocalFile();

          if (root.getName().startsWith(fileName)) {
            String newName = remoteChange.getContext(1);
            root.setName(root.getName().replaceFirst(fileName, newName));
            for (Snapshot sn : root.getChildren()) {
              addToSnapshot(sn, remoteChange);
            }
          } else {
            for (Snapshot sn : root.getChildren()) {
              if (fileName.startsWith(sn.getName())) {
                addToSnapshot(sn, remoteChange);
                return;
              }
            }
            // Didn't find?
            logger.error(
                MessageFormat.format(
                    "Remote rename cannot find local corresponding {0}, should be an error",
                    remoteChange));
            // throw new IllegalArgumentException();
          }
          break;
        }
      case REMOTE_DELETE:
        String fileName = remoteChange.getLocalFile();
        if (StringUtils.isEmpty(fileName)) {
          return;
        }
        for (int i = 0; i < root.getChildren().size(); i++) {
          Snapshot sn = root.getChildren().get(i);
          if (fileName.equals(sn.getName())) {
            root.getChildren().remove(sn);
            return;
          }
          if (fileName.startsWith(sn.getName())) {
            addToSnapshot(sn, remoteChange);
            return;
          }
        }
        // Didn't find?
        logger.warn(
            MessageFormat.format(
                "Remote change cannot find local corresponding {0}, possibly caused by deleting of parent folder",
                remoteChange));
        // This means the remote change is out of date
        break;
      default:
        throw new IllegalArgumentException();
    }
  }