Android Open Source - fh-android-sdk F H Sync Dataset






From Project

Back to project page fh-android-sdk.

License

The source code is released under:

Copyright (c) 2014 FeedHenry Ltd, All Rights Reserved. Please refer to your contract with FeedHenry for the software license agreement. If you do not have a contract, you do not have a license to use...

If you think the Android project fh-android-sdk listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

package com.feedhenry.sdk.sync;
/*from w ww. ja  v a 2  s  .  c o  m*/
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.json.fh.JSONArray;
import org.json.fh.JSONException;
import org.json.fh.JSONObject;

import android.content.Context;
import android.os.Message;

import com.feedhenry.sdk.FH;
import com.feedhenry.sdk.FHActCallback;
import com.feedhenry.sdk.FHResponse;
import com.feedhenry.sdk.api.FHActRequest;
import com.feedhenry.sdk.utils.FHLog;

public class FHSyncDataset {

  private boolean mSyncRunning;
  private boolean mInitialised;
  private String mDatasetId;
  private Date mSyncStart;
  private Date mSyncEnd;
  private boolean mSyncPending;
  private FHSyncConfig mSyncConfig = new FHSyncConfig();
  private ConcurrentMap<String, FHSyncPendingRecord> mPendingRecords = new ConcurrentHashMap<String, FHSyncPendingRecord>();
  private ConcurrentMap<String, FHSyncDataRecord> mDataRecords = new ConcurrentHashMap<String, FHSyncDataRecord>();
  private JSONObject mQueryParams = new JSONObject();
  private JSONObject mMetaData = new JSONObject();
  private String mHashvalue;
  private JSONArray mAcknowledgements = new JSONArray();
  private boolean mStopSync;
  
  private Context mContext;
  private FHSyncNotificationHandler mNotificationHandler;
  
  private static final String STORAGE_FILE_EXT = ".sync.json";
  
  private static final String KEY_DATESETID = "dataSetId";
  private static final String KEY_SYNC_LOOP_START = "syncLoopStart";
  private static final String KEY_SYNC_LOOP_END = "syncLoopEnd";
  private static final String KEY_SYNC_CONFIG = "syncConfig";
  private static final String KEY_PENDING_RECORDS = "pendingDataRecords";
  private static final String KEY_DATA_RECORDS = "dataRecords";
  private static final String KEY_HASHVALUE = "hashValue";
  private static final String KEY_ACKNOWLEDGEMENTS = "acknowledgements";
  private static final String KEY_QUERY_PARAMS = "queryParams";
  private static final String KEY_METADATA = "metaData";
  
  protected static final String LOG_TAG = "com.feedhenry.sdk.sync.FHSyncDataset";
  
  public FHSyncDataset(Context pContext, FHSyncNotificationHandler pHandler, String pDatasetId, FHSyncConfig pConfig, JSONObject pQueryParams){
    mContext = pContext;
    mNotificationHandler = pHandler;
    mDatasetId = pDatasetId;
    mSyncConfig = pConfig;
    mQueryParams = pQueryParams;
    readFromFile();
  }
  
  public JSONObject getJSON(){
    JSONObject ret = new JSONObject();
    if(null != mHashvalue){
      ret.put(KEY_HASHVALUE, mHashvalue);
    }
    ret.put(KEY_DATESETID, mDatasetId);
    ret.put(KEY_SYNC_CONFIG, mSyncConfig.getJSON());
    JSONObject pendingJson = new JSONObject();
    for(String key: mPendingRecords.keySet()){
      pendingJson.put(key, mPendingRecords.get(key).getJSON());
    }
    ret.put(KEY_PENDING_RECORDS, pendingJson);
    JSONObject dataJson = new JSONObject();
    for(String dkey: mDataRecords.keySet()){
      dataJson.put(dkey, mDataRecords.get(dkey).getJSON());
    }
    ret.put(KEY_DATA_RECORDS, dataJson);
    if(null != this.mSyncStart){
      ret.put(KEY_SYNC_LOOP_START, this.mSyncStart.getTime());
    }
    if(null != this.mSyncEnd){
      ret.put(KEY_SYNC_LOOP_END, this.mSyncEnd.getTime());
    }
    ret.put(KEY_ACKNOWLEDGEMENTS, mAcknowledgements);
    ret.put(KEY_QUERY_PARAMS, mQueryParams);
    ret.put(KEY_METADATA, mMetaData);
    return ret;
  }
  
  public JSONObject listData(){
    JSONObject ret = new JSONObject();
    for(String key: this.mDataRecords.keySet()){
      FHSyncDataRecord dataRecord = this.mDataRecords.get(key);
      JSONObject datajson = new JSONObject();
      //return a copy of the data so that any changes made to the data will not affect the original data
      datajson.put("data", new JSONObject(dataRecord.getData().toString()));
      datajson.put("uid", key);
      ret.put(key, datajson);
    }
    return ret;
  }
  
  public JSONObject readData(String pUid){
    FHSyncDataRecord dataRecord = mDataRecords.get(pUid);
    if(null != dataRecord){
      JSONObject ret = new JSONObject();
      //return a copy of the data so that any changes made to the data will not affect the original data
      ret.put("data", new JSONObject( dataRecord.getData().toString()));
      ret.put("uid", pUid);
      return ret;
    } else {
      return null;
    }
  }
  
  public JSONObject createData(JSONObject pData){
    FHSyncPendingRecord pendingRecord = addPendingObject(null, pData, "create");
    FHSyncDataRecord dataRecord = mDataRecords.get(pendingRecord.getUid());
    JSONObject ret = new JSONObject();
    if(null != dataRecord){
      ret.put("data", new JSONObject( dataRecord.getData().toString()));
      ret.put("uid", pendingRecord.getUid());
    }
    return ret;
  }
  
  public JSONObject updateData(String pUid, JSONObject pData){
    FHSyncPendingRecord pendingRecord = addPendingObject(pUid, pData, "update");
    FHSyncDataRecord dataRecord = mDataRecords.get(pUid);
    JSONObject ret = new JSONObject();
    if(null != dataRecord){
      ret.put("data", new JSONObject( dataRecord.getData().toString()));
      ret.put("uid", pUid);
    }
    return ret;
  }
  
  public JSONObject deleteData(String pUid){
    FHSyncPendingRecord pendingRecord = addPendingObject(pUid, null, "delete");
    FHSyncDataRecord deleted = pendingRecord.getPreData();
    JSONObject ret = new JSONObject();
    if(null != deleted){
      ret.put("data", new JSONObject( deleted.getData().toString()));
      ret.put("uid", pUid);
    }
    return ret;
  }
  
  public void startSyncLoop(){
    mSyncPending = false;
    mSyncRunning = true;
    mSyncStart = new Date();
    doNotify(null, NotificationMessage.SYNC_STARTED_CODE, null);
    if(!FH.isOnline()){
      syncCompleteWithCode("offline");
    } else {
      JSONObject syncLoopParams = new JSONObject();
      syncLoopParams.put("fn", "sync");
      syncLoopParams.put("dataset_id", mDatasetId);
      syncLoopParams.put("query_params", mQueryParams);
      if(null != mHashvalue){
        syncLoopParams.put("dataset_hash", mHashvalue);
      }
      syncLoopParams.put("acknowledgements", mAcknowledgements);
      JSONArray pendings = new JSONArray();
      for(String key: mPendingRecords.keySet()){
        FHSyncPendingRecord pendingRecord = mPendingRecords.get(key);
        if(!pendingRecord.isInFight() && !pendingRecord.isCrashed()){
          pendingRecord.setInFight(true);
          pendingRecord.setInFlightDate(new Date());
          JSONObject pendingJSON = pendingRecord.getJSON();
          pendingJSON.put("hash", pendingRecord.getHashValue());
          pendings.put(pendingJSON);
        }
      }
      
      syncLoopParams.put("pending", pendings);
      //if(pendings.length() > 0){
        FHLog.d(LOG_TAG, "Starting sync loop -global hash = " + mHashvalue + " :: params = " + syncLoopParams);
      //}
      
      try{
        FHActRequest actRequest = FH.buildActRequest(mDatasetId, syncLoopParams);
        actRequest.executeAsync(new FHActCallback() {
          
          @Override
          public void success(FHResponse pResponse) {
            JSONObject responseData = pResponse.getJson();
            syncRequestSuccess(responseData);
          }
          
          @Override
          public void fail(FHResponse pResponse) {
            // The AJAX call failed to complete succesfully, so the state of the current pending updates is unknown
            // Mark them as "crashed". The next time a syncLoop completets successfully, we will review the crashed
            // records to see if we can determine their current state.
            markInFlightAsCrashed();
            FHLog.e(LOG_TAG, "syncLoop failed : msg = " + pResponse.getErrorMessage(), pResponse.getError());
            doNotify(null, NotificationMessage.SYNC_FAILED_CODE, pResponse.getRawResponse());
            syncCompleteWithCode(pResponse.getRawResponse());
          }
        });
      } catch (Exception e){
        FHLog.e(LOG_TAG, "Error performing sync", e);
        doNotify(null, NotificationMessage.SYNC_FAILED_CODE, e.getMessage());
        syncCompleteWithCode(e.getMessage());
      }
      
    }
    
  }
  
  private void syncRequestSuccess(JSONObject pData){
    // Check to see if any new pending records need to be updated to reflect the current state of play.
    updatePendingFromNewData(pData);
    
    //Check to see if any previously crashed inflight records can now be resolved
    updateCrashedInFlightFromNewData(pData);
    
    //Update the new dataset with details of any inflight updates which we have not received a response on
    updateNewDataFromInFlight(pData);
    
    // Update the new dataset with details of any pending updates
    updateNewDataFromPending(pData);
    
    if(pData.has("records")){
      resetDataRecords(pData);
    }
    
    if(pData.has("updates")){
      JSONArray ack = new JSONArray();
      JSONObject updates = pData.getJSONObject("updates");
      processUpdates(updates.optJSONObject("applied"), NotificationMessage.REMOTE_UPDATE_APPLIED_CODE, ack);
      processUpdates(updates.optJSONObject("failed"), NotificationMessage.REMOTE_UPDATE_FAILED_CDOE, ack);
      processUpdates(updates.optJSONObject("collisions"), NotificationMessage.COLLISION_DETECTED_CDOE, ack);
      mAcknowledgements = ack;
    } else if(pData.has("hash") && !pData.getString("hash").equals(mHashvalue)){
      String remoteHash = pData.getString("hash");
      FHLog.d(LOG_TAG, "Local dataset stale - syncing records :: local hash= "+mHashvalue+" - remoteHash =" + remoteHash);
      // Different hash value returned - Sync individual records
      syncRecords();
    } else {
      FHLog.i(LOG_TAG, "LOcal dataset up to date");
    }
    
    syncCompleteWithCode("online");
  }
  
  private void syncRecords(){
    JSONObject clientRecords = new JSONObject();
    for(Map.Entry<String, FHSyncDataRecord> entry: mDataRecords.entrySet()){
      String uid = entry.getKey();
      FHSyncDataRecord record = entry.getValue();
      
      clientRecords.put(uid, record.getHashValue());
    }
    
    JSONObject syncRecsParams = new JSONObject();
    syncRecsParams.put("fn", "syncRecords");
    syncRecsParams.put("dataset_id", mDatasetId);
    syncRecsParams.put("query_params", mQueryParams);
    syncRecsParams.put("clientRecs", clientRecords);
    
    FHLog.d(LOG_TAG, "syncRecParams :: " + syncRecsParams);
    
    try{
      FHActRequest request = FH.buildActRequest(mDatasetId, syncRecsParams);
      request.executeAsync(new FHActCallback() {
        
        @Override
        public void success(FHResponse pResponse) {
          syncRecordsSuccess(pResponse.getJson());
        }
        
        @Override
        public void fail(FHResponse pResponse) {
          FHLog.e(LOG_TAG, "syncRecords failed: " + pResponse.getRawResponse(), pResponse.getError());
          doNotify(null, NotificationMessage.SYNC_FAILED_CODE, pResponse.getRawResponse());
          syncCompleteWithCode(pResponse.getRawResponse());
        }
      });
    } catch(Exception e){
      FHLog.e(LOG_TAG, "error when running syncRecords", e);
      doNotify(null, NotificationMessage.SYNC_FAILED_CODE, e.getMessage());
      syncCompleteWithCode(e.getMessage());
    }
    
  }
  
  private void syncRecordsSuccess(JSONObject pData){
    JSONObject dataCreated = pData.optJSONObject("create");
    if(null != dataCreated){
      Iterator<String> dit = dataCreated.keys();
      while(dit.hasNext()){
        String dk = dit.next();
        JSONObject data = dataCreated.getJSONObject(dk);
        FHSyncDataRecord rec = new FHSyncDataRecord(data.getJSONObject("data"));
        rec.setHashValue(data.getString("hash"));
        mDataRecords.put(dk, rec);
        doNotify(dk, NotificationMessage.DELTA_RECEIVED_CODE, "create");
      }
    }
    
    JSONObject dataUpdated = pData.optJSONObject("update");
    if(null != dataUpdated){
      Iterator<String> uit = dataUpdated.keys();
      while(uit.hasNext()){
        String uk = uit.next();
        JSONObject updatedata = dataUpdated.getJSONObject(uk);
        FHSyncDataRecord urec = mDataRecords.get(uk);
        if(null != urec){
          urec.setData(updatedata.getJSONObject("data"));
          urec.setHashValue(updatedata.getString("hash"));
          mDataRecords.put(uk, urec);
          doNotify(uk, NotificationMessage.DELTA_RECEIVED_CODE, "update");
        }
      }
    }
    
    JSONObject deleted = pData.optJSONObject("delete");
    if(null != deleted){
      Iterator<String> rit = deleted.keys();
      while(rit.hasNext()){
        String rkey = rit.next();
        mDataRecords.remove(rkey);
        doNotify(rkey, NotificationMessage.DELTA_RECEIVED_CODE, "delete");
      }
    }
    
    if(pData.has("hash")){
      mHashvalue = pData.getString("hash");
    }
    
    syncCompleteWithCode("online");
  }
  
  
  private void processUpdates(JSONObject pUpdates, int pNotification, JSONArray pAck){
    if(null != pUpdates){
      Iterator<String> it = pUpdates.keys();
      while(it.hasNext()){
        String key = it.next();
        JSONObject up = pUpdates.getJSONObject(key);
        pAck.put(up);
        FHSyncPendingRecord pendingRec = mPendingRecords.get(key);
        if(null != pendingRec && pendingRec.isInFight() && !pendingRec.isCrashed()){
          mPendingRecords.remove(key);
          doNotify(up.getString("uid"), pNotification, up.toString());
        }
      }
    }
  }
  
  private void resetDataRecords(JSONObject pData){
    JSONObject records = pData.getJSONObject("records");
    ConcurrentMap<String, FHSyncDataRecord> allrecords = new ConcurrentHashMap<String, FHSyncDataRecord>();
    
    Iterator<String> it = records.keys();
    while(it.hasNext()){
      String key = it.next();
      JSONObject data = records.getJSONObject(key);
      FHSyncDataRecord record = new FHSyncDataRecord(data.getJSONObject("data"));
      allrecords.put(key, record);
    }
    
    mDataRecords = allrecords;
    mHashvalue = pData.getString("hash");
    doNotify(mHashvalue, NotificationMessage.DELTA_RECEIVED_CODE, "full dataset");
  }
  
  private void updatePendingFromNewData(JSONObject pData){
    if(null != mPendingRecords && pData.has("records")){
      for(Map.Entry<String, FHSyncPendingRecord> entry: mPendingRecords.entrySet()){
        FHSyncPendingRecord pendingRecord = entry.getValue();
        JSONObject metadata = mMetaData.optJSONObject(pendingRecord.getUid());
        if(null == metadata){
          metadata = new JSONObject();
          mMetaData.put(pendingRecord.getUid(), metadata);
        }
        if(!pendingRecord.isInFight()){
          //pending record that has not been submitted
          FHLog.d(LOG_TAG, "updatePendingFromNewData - Found Non inFlight record -> action = " + pendingRecord.getAction() + " :: uid = " + pendingRecord.getUid() + " :: hash = " + pendingRecord.getHashValue());
          if("update".equalsIgnoreCase(pendingRecord.getAction()) || "delete".equalsIgnoreCase(pendingRecord.getAction())){
            // Update the pre value of pending record to reflect the latest data returned from sync.
            JSONObject remoteRec = pData.getJSONObject("records").optJSONObject(pendingRecord.getUid());
            if(null != remoteRec){
              FHLog.d(LOG_TAG, "updatePendingFromNewData - updating pre values for existing pending record " + pendingRecord.getUid());
              FHSyncDataRecord rec = new FHSyncDataRecord(remoteRec);
              pendingRecord.setPreData(rec);
            } else {
              //The update/delete may be for a newly created record in which case the uid will be changed.
              String previousPendingUid = metadata.optString("previousPendingUid");
              if(null != previousPendingUid){
                FHSyncPendingRecord previousPendingRec = mPendingRecords.get(previousPendingUid);
                if(null != previousPendingRec){
                  if(pData.has("updates")){
                    JSONObject updates = pData.getJSONObject("updates");
                    if(updates.has("applied")){
                      JSONObject applied = updates.getJSONObject("applied");
                      if(applied.has(previousPendingRec.getHashValue())){
                        //There is an update in from a previous pending action
                        String remoteUid = applied.getJSONObject(previousPendingRec.getHashValue()).optString("uid", null);
                        if(null != remoteUid){
                          remoteRec = pData.getJSONObject("records").optJSONObject(remoteUid);
                          if(null != remoteRec){
                            FHLog.d(LOG_TAG, "updatePendingFromNewData - Updating pre values for existing pending record which was previously a create " + pendingRecord.getUid() + " => " + remoteUid);
                            FHSyncDataRecord record = new FHSyncDataRecord(remoteRec);
                            pendingRecord.setPreData(record);
                            pendingRecord.setUid(remoteUid);
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
        
        String pendingHash = entry.getKey();
        if("create".equalsIgnoreCase(pendingRecord.getAction())){
          if(null != pData && pData.has("updates")){
            JSONObject updates = pData.getJSONObject("updates");
            if(updates.has("applied")){
              JSONObject applied = updates.getJSONObject("applied");
              if(applied.has(pendingHash)){
                JSONObject appliedData = applied.getJSONObject(pendingHash);
                FHLog.d(LOG_TAG, "updatePendingFromNewData - Found an update for a pending create " + appliedData);
                JSONObject remoteRec = pData.optJSONObject(applied.optString("uid", ""));
                if(null != remoteRec){
                  FHLog.d(LOG_TAG, "updatePendingFromNewData - Changing pending create to an update based on new record " + remoteRec);
                  //setup the pending as an update
                  pendingRecord.setAction("update");
                  FHSyncDataRecord preData = new FHSyncDataRecord(remoteRec);
                  pendingRecord.setPreData(preData);
                  pendingRecord.setUid(applied.optString("uid"));
                }
              }
            }
          }
        }
        
      }
    }
  }
  
  private void updateCrashedInFlightFromNewData(JSONObject pData){
    JSONObject updateNotifications = new JSONObject();
    updateNotifications.put("applied", NotificationMessage.REMOTE_UPDATE_APPLIED_CODE);
    updateNotifications.put("failed", NotificationMessage.REMOTE_UPDATE_FAILED_CDOE);
    updateNotifications.put("collisions", NotificationMessage.COLLISION_DETECTED_CDOE);
    
    JSONObject resolvedCrashed = new JSONObject();
    List<String> keysToRemove = new ArrayList<String>();
    
    for(Map.Entry<String, FHSyncPendingRecord> entry: mPendingRecords.entrySet()){
      FHSyncPendingRecord pendingRecord = entry.getValue();
      String pendingHash = entry.getKey();
      if(pendingRecord.isInFight() && pendingRecord.isCrashed()){
        FHLog.d(LOG_TAG, "updateCrashedInFlightFromNewData - Found crashed inFlight pending record uid= " + pendingRecord.getUid() + ":: hash = " + pendingRecord.getHashValue());
        if(null != pData && pData.has("updates")){
          JSONObject updates = pData.getJSONObject("updates");
          if(updates.has("hashes")){
            JSONObject hashes = updates.getJSONObject("hashes");
            //check if the updates received contain any info about the crashed inflight update
            JSONObject crashedUpdate = hashes.optJSONObject(pendingHash);
            if(null != crashedUpdate){
              resolvedCrashed.put(crashedUpdate.optString("uid"), crashedUpdate);
              FHLog.d(LOG_TAG, "updateCrashedInFlightFromNewData - Resolving status for crashed inflight pending record " + crashedUpdate);
              String crashedType = crashedUpdate.optString("type", null);
              String crashedAction = crashedUpdate.optString("action", null);
              if(null != crashedType && "failed".equalsIgnoreCase(crashedType)){
                //Crashed updated failed - revert local dataset
                if(null != crashedAction && "create".equalsIgnoreCase(crashedAction)){
                  FHLog.d(LOG_TAG, "updateCrashedInFlightFromNewData - Deleting failed create from dataset");
                  mDataRecords.remove(crashedUpdate.getString("uid"));
                } else if(null != crashedAction && ("update".equalsIgnoreCase(crashedAction) || "delete".equalsIgnoreCase(crashedAction))){
                  FHLog.d(LOG_TAG, "updateCrashedInFlightFromNewData - Reverting failed " +crashedAction+ " in dataset");
                  mDataRecords.put(crashedUpdate.getString("uid"), pendingRecord.getPreData());
                }
              }
              
              keysToRemove.add(pendingHash);
              doNotify(crashedUpdate.getString("uid"), updateNotifications.getInt(crashedUpdate.getString("type")), crashedUpdate.toString());
              
            } else {
               // No word on our crashed update - increment a counter to reflect another sync that did not give us
              // any update on our crashed record.
              pendingRecord.incrementCrashCount();
            }
          } else {
            // No word on our crashed update - increment a counter to reflect another sync that did not give us
            // any update on our crashed record.
            pendingRecord.incrementCrashCount();
          }
        } else {
          pendingRecord.incrementCrashCount();
        }
      }
    }
    
    for(int i=0;i<keysToRemove.size();i++){
      mPendingRecords.remove(keysToRemove.get(i));
    }
    
    keysToRemove.clear();
    
    for(Map.Entry<String, FHSyncPendingRecord> entry: mPendingRecords.entrySet()){
      FHSyncPendingRecord pendingRecord = entry.getValue();
      String pendingHash = entry.getKey();
      if(pendingRecord.isInFight() && pendingRecord.isCrashed()){
        if(pendingRecord.getCrashedCount() > mSyncConfig.getCrashCountWait()){
          FHLog.d(LOG_TAG, "updateCrashedInFlightFromNewData - Crashed inflight pending record has reached crashed_count_wait limit : " + pendingRecord);
          if(mSyncConfig.isResendCrashedUpdates()){
            FHLog.d(LOG_TAG, "updateCrashedInFlightFromNewData - Retryig crashed inflight pending record");
            pendingRecord.setCrashed(false);
            pendingRecord.setInFight(false);
          } else {
            FHLog.d(LOG_TAG, "updateCrashedInFlightFromNewData - Deleting crashed inflight pending record");
            keysToRemove.add(pendingHash);
          }
        }
      } else if(!pendingRecord.isInFight() && pendingRecord.isCrashed()){
        FHLog.d(LOG_TAG, "updateCrashedInFlightFromNewData - Trying to resolve issues with crashed non in flight record - uid =" + pendingRecord.getUid());
        // Stalled pending record because a previous pending update on the same record crashed
        JSONObject data = resolvedCrashed.optJSONObject(pendingRecord.getUid());
        if(null != data){
          FHLog.d(LOG_TAG, "updateCrashedInFlightFromNewData - Found a stalled pending record backed up behind a resolved crash uid=" + pendingRecord.getUid() +  " :: hash=" + pendingRecord.getHashValue());
          pendingRecord.setCrashed(false);
        }
      }
    }
    
    for(int i=0;i<keysToRemove.size();i++){
      mPendingRecords.remove(keysToRemove.get(i));
    }
  }
  
  public void updateNewDataFromInFlight(JSONObject pData){
    if(null != mPendingRecords && pData.has("records")){
      for(Map.Entry<String, FHSyncPendingRecord> entry: mPendingRecords.entrySet()){
        FHSyncPendingRecord pendingRecord = entry.getValue();
        String pendingHash = entry.getKey();
        
        if(pendingRecord.isInFight()){
          boolean updateReceivedForPending = false;
          if(pData.has("updates")){
            JSONObject updates = pData.getJSONObject("updates");
            if(updates.has("hashes")){
              JSONObject hashes = updates.getJSONObject("hashes");
              if(hashes.has(pendingHash)){
                updateReceivedForPending = true;
              }
            }
          }
          FHLog.d(LOG_TAG, "updateNewDataFromInFlight - Found inflight pending Record - action = " + pendingRecord.getAction() + " :: hash = " +  pendingRecord.getHashValue() + " :: updateReceivedForPending= " + updateReceivedForPending);
          if(!updateReceivedForPending){
            JSONObject remoteRecord = pData.getJSONObject("records").optJSONObject(pendingRecord.getUid());
            if("update".equalsIgnoreCase(pendingRecord.getAction()) && null != remoteRecord){
              // Modify the new Record to have the updates from the pending record so the local dataset is consistent
              remoteRecord.put("data", pendingRecord.getPostData().getData());
              remoteRecord.put("hash", pendingRecord.getPostData().getHashValue());
            } else if("delete".equalsIgnoreCase(pendingRecord.getAction()) && null != remoteRecord){
              // Remove the record from the new dataset so the local dataset is consistent
              pData.getJSONObject("records").remove(pendingRecord.getUid());
            } else if("create".equalsIgnoreCase(pendingRecord.getAction())){
               // Add the pending create into the new dataset so it is not lost from the UI
              FHLog.d(LOG_TAG, "updateNewDataFromInFlight - re adding pending create to incomming dataset");
              JSONObject dict = new JSONObject();
              dict.put("data", pendingRecord.getPostData().getData());
              dict.put("hash", pendingRecord.getPostData().getHashValue());
              pData.getJSONObject("records").put(pendingRecord.getUid(), dict);
            }
          }
        }
      }
    }
  }
  
  private void updateNewDataFromPending(JSONObject pData){
    if(null != mPendingRecords && pData.has("records")){
      for(Map.Entry<String, FHSyncPendingRecord> entry: mPendingRecords.entrySet()){
        FHSyncPendingRecord pendingRecord = entry.getValue();
        if(!pendingRecord.isInFight()){
          FHLog.d(LOG_TAG, "updateNewDataFromPending - Found Non inFlight record -> action="+pendingRecord.getAction()+" :: uid="+pendingRecord.getUid()+" :: hash=" + pendingRecord.getHashValue());
          JSONObject remoteRecord = pData.getJSONObject("records").optJSONObject(pendingRecord.getUid());
          if(null != remoteRecord && "update".equalsIgnoreCase(pendingRecord.getAction())){
            // Modify the new Record to have the updates from the pending record so the local dataset is consistent
            remoteRecord.put("data", pendingRecord.getPostData().getData());
            remoteRecord.put("hash", pendingRecord.getPostData().getHashValue());
          } else if(null != remoteRecord && "delete".equalsIgnoreCase(pendingRecord.getAction())){
            pData.getJSONObject("records").remove(pendingRecord.getUid());
          } else if("create".equalsIgnoreCase(pendingRecord.getAction())){
            // Add the pending create into the new dataset so it is not lost from the UI
            FHLog.d(LOG_TAG, "updateNewDataFromPending - re adding pending create to incomming dataset");
            JSONObject dict = new JSONObject();
            dict.put("data", pendingRecord.getPostData().getData());
            dict.put("hash", pendingRecord.getPostData().getHashValue());
            pData.getJSONObject("records").put(pendingRecord.getUid(), dict);
          }
        }
      }
    }
  }
  
  private void markInFlightAsCrashed(){
    Map<String, FHSyncPendingRecord> crashedRecords = new HashMap<String, FHSyncPendingRecord>();
    for(Map.Entry<String, FHSyncPendingRecord> entry: mPendingRecords.entrySet()){
      FHSyncPendingRecord pendingRecord = entry.getValue();
      String pendingHash = entry.getKey();
      if(pendingRecord.isInFight()){
        FHLog.d(LOG_TAG, "Marking in flight pending record as crashed : " + pendingHash);
        pendingRecord.setCrashed(true);
        crashedRecords.put(pendingRecord.getUid(), pendingRecord);
      }
    }
    
    // Check for any pending updates that would be modifying a crashed record. These can not go out until the
    // status of the crashed record is determined
    for(Map.Entry<String, FHSyncPendingRecord> entry: mPendingRecords.entrySet()){
      FHSyncPendingRecord pendingRecord = entry.getValue();
      if(!pendingRecord.isInFight()){
        if(crashedRecords.containsKey(pendingRecord.getUid())){
          pendingRecord.setCrashed(true);
        }
      }
    }
  }
  
  public void syncCompleteWithCode(String pCode){
    mSyncRunning = false;
    mSyncEnd = new Date();
    writeToFile();
    doNotify(mHashvalue, NotificationMessage.SYNC_COMPLETE_CODE, pCode);
  }
  
  private FHSyncPendingRecord addPendingObject(String pUid, JSONObject pData, String pAction){
    if(!FH.isOnline()){
      doNotify(pUid, NotificationMessage.OFFLINE_UPDATE_CODE, pAction);
    }
    FHSyncPendingRecord pending = new FHSyncPendingRecord();
    pending.setInFight(false);
    pending.setAction(pAction);
    
    if(null != pData){
      FHSyncDataRecord dataRecord = new FHSyncDataRecord(pData);
      pending.setPostData(dataRecord);
    }
    
    if("create".equalsIgnoreCase(pAction)){
      pending.setUid(pending.getPostData().getHashValue());
      storePendingObj(pending);
    } else {
      FHSyncDataRecord existingData = mDataRecords.get(pUid);
      if(null != existingData){
        pending.setUid(pUid);
        pending.setPreData(existingData.clone());
        storePendingObj(pending);
      }
    }
    return pending;
  }
  
  private void storePendingObj(FHSyncPendingRecord pPendingObj){
    mPendingRecords.put(pPendingObj.getHashValue(), pPendingObj);
    updateDatasetFromLocal(pPendingObj);
    if(mSyncConfig.isAutoSyncLocalUpdates()){
      mSyncPending = true;
    }
    writeToFile();
    doNotify(pPendingObj.getUid(), NotificationMessage.LOCAL_UPDATE_APPLIED_CODE, pPendingObj.getAction());
  }
  
  private void updateDatasetFromLocal(FHSyncPendingRecord pPendingObj){
    String previousPendingUid = null;
    FHSyncPendingRecord previousPendingObj = null;
    String uid = pPendingObj.getUid();
    FHLog.d(LOG_TAG, "updating local dataset for uid " + uid + " - action = " + pPendingObj.getAction());
    JSONObject metadata = mMetaData.optJSONObject(uid);
    if(null == metadata){
      metadata = new JSONObject();
      mMetaData.put(uid,metadata);
    }
    FHSyncDataRecord existing = mDataRecords.get(uid);
    boolean fromPending = metadata.optBoolean("fromPending");
    if("create".equalsIgnoreCase(pPendingObj.getAction())){
      if(null != existing){
        FHLog.d(LOG_TAG, "dataset already exists for uid for create :: " + existing.toString());
        if(fromPending){
          // We are trying to create on top of an existing pending record
          // Remove the previous pending record and use this one instead
          previousPendingUid = metadata.optString("pendingUid", null);
          if(null != previousPendingUid){
            mPendingRecords.remove(previousPendingUid);
          }
        }
      }
      mDataRecords.put(uid, new FHSyncDataRecord());
    }
    
    if("update".equalsIgnoreCase(pPendingObj.getAction())){
      if(null != existing){
        if(fromPending){
          FHLog.d(LOG_TAG, "Updating an existing pending record for dataset :: " + existing.toString());
          // We are trying to update an existing pending record
          previousPendingUid = metadata.optString("pendingUid", null);
          metadata.put("previousPendingUid", previousPendingUid);
          previousPendingObj = mPendingRecords.get(previousPendingUid);
          if(null != previousPendingObj && !previousPendingObj.isInFight()){
            FHLog.d(LOG_TAG, "existing pre-flight pending record = " + previousPendingObj);
            // We are trying to perform an update on an existing pending record
            // modify the original record to have the latest value and delete the pending update
            previousPendingObj.setPostData(pPendingObj.getPostData());
            mPendingRecords.remove(pPendingObj.getHashValue());
          }
        }
      }
    }
    
    if("delete".equalsIgnoreCase(pPendingObj.getAction())){
      if(null != existing && fromPending){
        FHLog.d(LOG_TAG, "Deleting an existing pending record for dataset :: " + existing);
        // We are trying to delete an existing pending record
        previousPendingUid = metadata.optString("pendingUid", null);
        metadata.put("previousPendingUid", previousPendingUid);
        previousPendingObj = mPendingRecords.get(previousPendingUid);
        if(null != previousPendingObj && !previousPendingObj.isInFight()){
          FHLog.d(LOG_TAG, "existing pending record = " + previousPendingObj);
          if("create".equalsIgnoreCase(previousPendingObj.getAction())){
            // We are trying to perform a delete on an existing pending create
            // These cancel each other out so remove them both
            mPendingRecords.remove(pPendingObj.getHashValue());
            mPendingRecords.remove(previousPendingUid);
          }
          if("update".equalsIgnoreCase(previousPendingObj.getAction())){
            // We are trying to perform a delete on an existing pending update
            // Use the pre value from the pending update for the delete and
            // get rid of the pending update
            pPendingObj.setPreData(previousPendingObj.getPreData());
            pPendingObj.setInFight(false);
            mPendingRecords.remove(previousPendingUid);
          }
        }
      }
      mDataRecords.remove(uid);
    }
    
    if(mDataRecords.containsKey(uid)){
      FHSyncDataRecord record = pPendingObj.getPostData();
      mDataRecords.put(uid, record);
      metadata.put("fromPending", true);
      metadata.put("pendingUid", pPendingObj.getHashValue());
    }
  }
  
  private void fromJSON(JSONObject pObj){
    JSONObject syncConfigJson = pObj.getJSONObject(KEY_SYNC_CONFIG);
    FHSyncConfig syncConfig = FHSyncConfig.fromJSON(syncConfigJson);
    this.mSyncConfig = syncConfig;
    this.mHashvalue = pObj.optString(KEY_HASHVALUE, null);
    JSONObject pendingJSON = pObj.getJSONObject(KEY_PENDING_RECORDS);
    Iterator<String> it = pendingJSON.keys();
    while(it.hasNext()){
      String key = it.next();
      JSONObject pendObjJson = pendingJSON.getJSONObject(key);
      FHSyncPendingRecord pending = FHSyncPendingRecord.fromJSON(pendObjJson);
      this.mPendingRecords.put(key, pending);
    }
    JSONObject dataJSON = pObj.getJSONObject(KEY_DATA_RECORDS);
    Iterator<String> dit = dataJSON.keys();
    while(dit.hasNext()){
      String dkey = dit.next();
      JSONObject dataObjJson = dataJSON.getJSONObject(dkey);
      FHSyncDataRecord datarecord = FHSyncDataRecord.fromJSON(dataObjJson);
      this.mDataRecords.put(dkey, datarecord);
    }
    if(pObj.has(KEY_SYNC_LOOP_START)){
      this.mSyncStart = new Date(pObj.getLong(KEY_SYNC_LOOP_START));
    }
    if(pObj.has(KEY_SYNC_LOOP_END)){
      this.mSyncEnd = new Date(pObj.getLong(KEY_SYNC_LOOP_END));
    }
    if(pObj.has(KEY_ACKNOWLEDGEMENTS)){
      this.mAcknowledgements = pObj.getJSONArray(KEY_ACKNOWLEDGEMENTS);
    }
    if(pObj.has(KEY_QUERY_PARAMS)){
      this.mQueryParams = pObj.getJSONObject(KEY_QUERY_PARAMS);
    }
    if(pObj.has(KEY_METADATA)){
      this.mMetaData = pObj.getJSONObject(KEY_METADATA);
    }
  }
  
  
  private void readFromFile(){
    String filePath = mDatasetId + STORAGE_FILE_EXT;
    try{
      FileInputStream fis = mContext.openFileInput(filePath);
      ByteArrayOutputStream bos = new ByteArrayOutputStream();
      writeStram(fis, bos);
      String content = bos.toString("UTF-8");
      JSONObject json = new JSONObject(content);
      fromJSON(json);
      doNotify(null, NotificationMessage.LOCAL_UPDATE_APPLIED_CODE, "load");
    } catch(FileNotFoundException ex){
      FHLog.w(LOG_TAG, "File not found for reading: " + filePath);
    } catch(IOException e){
      FHLog.e(LOG_TAG, "Error reading file : " + filePath, e);
    } catch(JSONException je){
      FHLog.e(LOG_TAG, "Failed to parse JSON file : " + filePath, je);
    }
  }
  
  public synchronized void writeToFile() {
    String filePath = mDatasetId + STORAGE_FILE_EXT;
    try{
      FileOutputStream fos = mContext.openFileOutput(filePath, Context.MODE_PRIVATE);
      String content = getJSON().toString();
      ByteArrayInputStream bis = new ByteArrayInputStream(content.getBytes());
      writeStram(bis, fos);
    } catch(FileNotFoundException ex){
      FHLog.e(LOG_TAG, "File not found for writing: " + filePath, ex);
      doNotify(null, NotificationMessage.CLIENT_STORAGE_FAILED_CODE, null);
    } catch(IOException e){
      FHLog.e(LOG_TAG, "Error writing file: " + filePath, e);
      doNotify(null, NotificationMessage.CLIENT_STORAGE_FAILED_CODE, null);
    }
  }
  
  private void doNotify(String pUID, int pCode, String pMessage){
    boolean sendMessage = false;
    switch(pCode){
    case NotificationMessage.SYNC_STARTED_CODE:
      if(mSyncConfig.isNotifySyncStarted()){
        sendMessage = true;
      }
      break;
    case NotificationMessage.SYNC_COMPLETE_CODE:
      if(mSyncConfig.isNotifySyncComplete()){
        sendMessage = true;
      }
      break;
    case NotificationMessage.OFFLINE_UPDATE_CODE:
      if(mSyncConfig.isNotifyOfflineUpdate()){
        sendMessage = true;
      }
      break;
    case NotificationMessage.COLLISION_DETECTED_CDOE:
      if(mSyncConfig.isNotifySyncCollisions()){
        sendMessage = true;
      }
      break;
    case NotificationMessage.REMOTE_UPDATE_FAILED_CDOE:
      if(mSyncConfig.isNotifyUpdateFailed()){
        sendMessage = true;
      }
      break;
    case NotificationMessage.REMOTE_UPDATE_APPLIED_CODE:
      if(mSyncConfig.isNotifyRemoteUpdateApplied()){
        sendMessage = true;
      }
      break;
    case NotificationMessage.LOCAL_UPDATE_APPLIED_CODE:
      if(mSyncConfig.isNotifyLocalUpdateApplied()){
        sendMessage = true;
      }
        break;
    case NotificationMessage.DELTA_RECEIVED_CODE:
      if(mSyncConfig.isNotifyDeltaReceived()){
        sendMessage = true;
      }
      break;
    case NotificationMessage.SYNC_FAILED_CODE:
      if(mSyncConfig.isNotifySyncFailed()){
        sendMessage = true;
      }
      break;
    case NotificationMessage.CLIENT_STORAGE_FAILED_CODE:
      if(mSyncConfig.isNotifyClientStorageFailed()){
        sendMessage = true;
      }
    default: 
      break;
    }
    if(sendMessage){
      NotificationMessage notification = NotificationMessage.getMessage(mDatasetId, pUID, pCode, pMessage);
      Message message = mNotificationHandler.obtainMessage(pCode, notification);
      mNotificationHandler.sendMessage(message);
    }
  }
  
  private void writeStram(InputStream pInput, OutputStream pOutput) throws IOException {
    if(null != pInput && null != pOutput){
      BufferedInputStream bis = new BufferedInputStream(pInput);
      BufferedOutputStream bos = new BufferedOutputStream(pOutput);
      byte[] buffer = new byte[1024];
      int count;
      while((count = bis.read(buffer)) != -1){
        bos.write(buffer, 0, count);
      }
      bos.close();
      bis.close();
    }
  }

  public void setSyncRunning(boolean pSyncRunning) {
    this.mSyncRunning = pSyncRunning;
  }
  
  public boolean isSyncRunning(){
    return mSyncRunning;
  }

  public void setInitialised(boolean pInitialised) {
    this.mInitialised = pInitialised;
  }

  public void setSyncPending(boolean pSyncPending) {
    this.mSyncPending = pSyncPending;
  }
  
  public boolean isSyncPending(){
    return mSyncPending;
  }

  public void setSyncConfig(FHSyncConfig pSyncConfig) {
    this.mSyncConfig = pSyncConfig;
  }
  
  public FHSyncConfig getSyncConfig(){
    return mSyncConfig;
  }

  public void setQueryParams(JSONObject pQueryParams) {
    this.mQueryParams = pQueryParams;
  }
  
  public void stopSync(boolean pStopSync){
    this.mStopSync = pStopSync;
  }
  
  public boolean isStopSync(){
    return mStopSync;
  }
  
  public Date getSyncStart(){
    return mSyncStart;
  }
  
  public Date getSyncEnd(){
    return mSyncEnd;
  }
  
  public void setContext(Context pContext){
    mContext = pContext;
  }
  
  public void setNotificationHandler(FHSyncNotificationHandler pHandler){
    mNotificationHandler = pHandler;
  }
  
}




Java Source Code List

com.feedhenry.fhandroidexampleapp.FHActActivity.java
com.feedhenry.fhandroidexampleapp.FHAndroidExampleActivity.java
com.feedhenry.fhandroidexampleapp.FHAuthActivity.java
com.feedhenry.fhandroidexampleapp.FHLoginActivity.java
com.feedhenry.fhandroidexampleapp.FHSyncActivity.java
com.feedhenry.fhandroidexampleapp.FhUtil.java
com.feedhenry.fhandroidexampleapp.ItemDetailsActivity.java
com.feedhenry.fhandroidexampleapp.SyncCollisionResolveActivity.java
com.feedhenry.fhandroidexampleapp.SyncCollisionsListActivity.java
com.feedhenry.sdk.CloudProps.java
com.feedhenry.sdk.FHActCallback.java
com.feedhenry.sdk.FHAct.java
com.feedhenry.sdk.FHHttpClient.java
com.feedhenry.sdk.FHRemote.java
com.feedhenry.sdk.FHResponse.java
com.feedhenry.sdk.FH.java
com.feedhenry.sdk.api.FHActRequest.java
com.feedhenry.sdk.api.FHAuthRequest.java
com.feedhenry.sdk.api.FHCloudRequest.java
com.feedhenry.sdk.api.FHInitializeRequest.java
com.feedhenry.sdk.exceptions.FHInvalidActionException.java
com.feedhenry.sdk.exceptions.FHNotReadyException.java
com.feedhenry.sdk.oauth.FHOAuthIntent.java
com.feedhenry.sdk.oauth.FHOAuthWebView.java
com.feedhenry.sdk.sync.FHSyncClient.java
com.feedhenry.sdk.sync.FHSyncConfig.java
com.feedhenry.sdk.sync.FHSyncDataRecord.java
com.feedhenry.sdk.sync.FHSyncDataset.java
com.feedhenry.sdk.sync.FHSyncListener.java
com.feedhenry.sdk.sync.FHSyncNotificationHandler.java
com.feedhenry.sdk.sync.FHSyncPendingRecord.java
com.feedhenry.sdk.sync.FHSyncUtils.java
com.feedhenry.sdk.sync.NotificationMessage.java
com.feedhenry.sdk.utils.FHLog.java
com.feedhenry.starter.FHStarterActivity.java
com.loopj.android.http.AsyncHttpClient.java
com.loopj.android.http.AsyncHttpRequest.java
com.loopj.android.http.AsyncHttpResponseHandler.java
com.loopj.android.http.Base64DataException.java
com.loopj.android.http.Base64OutputStream.java
com.loopj.android.http.Base64.java
com.loopj.android.http.BaseJsonHttpResponseHandler.java
com.loopj.android.http.BinaryHttpResponseHandler.java
com.loopj.android.http.DataAsyncHttpResponseHandler.java
com.loopj.android.http.FileAsyncHttpResponseHandler.java
com.loopj.android.http.JsonHttpResponseHandler.java
com.loopj.android.http.JsonStreamerEntity.java
com.loopj.android.http.MyRedirectHandler.java
com.loopj.android.http.MySSLSocketFactory.java
com.loopj.android.http.PersistentCookieStore.java
com.loopj.android.http.PreemtiveAuthorizationHttpRequestInterceptor.java
com.loopj.android.http.RangeFileAsyncHttpResponseHandler.java
com.loopj.android.http.RequestHandle.java
com.loopj.android.http.RequestParams.java
com.loopj.android.http.ResponseHandlerInterface.java
com.loopj.android.http.RetryHandler.java
com.loopj.android.http.SerializableCookie.java
com.loopj.android.http.SimpleMultipartEntity.java
com.loopj.android.http.SyncHttpClient.java
com.loopj.android.http.TextHttpResponseHandler.java
org.json.fh.CDL.java
org.json.fh.CookieList.java
org.json.fh.Cookie.java
org.json.fh.HTTPTokener.java
org.json.fh.HTTP.java
org.json.fh.JSONArray.java
org.json.fh.JSONException.java
org.json.fh.JSONObject.java
org.json.fh.JSONString.java
org.json.fh.JSONStringer.java
org.json.fh.JSONTokener.java
org.json.fh.JSONWriter.java
org.json.fh.XMLTokener.java
org.json.fh.XML.java