using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authorization; using Microsoft.EntityFrameworkCore; using GZTW.Pecklist.Models; using GZTW.Pecklist.Util; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Microsoft.Extensions.Logging; namespace GZTW.Pecklist.Controllers { [Produces("application/json")]//forces all responses to be json [Route("api/sync")] [Authorize] public class SyncController : Controller { private readonly PecklistContext _ct; private readonly ILogger _logger; public SyncController(PecklistContext context, ILogger logger) { _ct = context; //guard clause _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } // POST: api/sync [HttpPost] public ActionResult PostList([FromBody] JArray jclient) { //TODO: Might need a lock flag in db here just in case of duelling updates //Get list from server ListData l = _ct.ListData.First(c=>c.Id == 1); //client has no data, send them the latest data if (jclient == null) { return Ok(l.TheList); } JArray jserver = JArray.Parse(l.TheList); JArray jnew = doSync(jclient, jserver); l.TheList = jnew.ToString(); _ct.ListData.Update(l); _ct.SaveChanges(); return Ok(l.TheList); } //////////////////////////////////////////////////////////////////////// // SYNCHRONIZE //////////////////////////////////////////////////////////////////////// /* SYNC STRATEGY =-=-=-=-=-=-= NEW ITEMS: client record is iterated looking for id that starts with the string "NEW_". If any are found the master rev is incremented and new items are added with a proper ID. DELETED ITEMS: Server record is itereated looking for deleted items (items with priority -1), if any are found master rev is incremented (unless it was already) then they are removed from the master list. UPDATE Compare client record to master record, compare list name for a change, then find all items that have the same ID but different user editable values, if any found update master rev if not done already, make master changed items match client changed items, update master. RETURN: Finally, if master list has changed, it is saved to db. Master list is then returned returned to client. */ ///////////////////////////////////////////// // Synchronize the two lists, return the new // private JArray doSync(JArray jclient, JArray jserver) { _logger.LogInformation(LoggingEvents.SyncList, "Synchronize list"); //Iterate the client's lists looking for changes foreach (JObject l in jclient) { //track if master v is incremented (non zero) long newListVersion = 0; var listId = (string)l["id"]; //Is this an entirely new list? if (listId.StartsWith("NEW_")) { //yes this is a new list, add it and go to the next addNewList(l, jserver); continue; } //bugbug: this will bomb if client syncs with a list that was deleted (which should be entirely ignored) ListHeaderData oldListHeader = getListHeaderData(listId, jserver); if (oldListHeader.v == -999) { //list isn't at the server, was deleted so go to next list and ignore it continue; } //Has this list been deleted? long newListV = (long)l["v"]; if (l["deleted"] != null && newListV == oldListHeader.v) { removeList(listId, jserver); continue; } //Has the name changed? string newListName = (string)l["name"]; long newListNameV = (long)l["name_v"]; //if names differ but name_v are the same then the client changed the name if (newListName != oldListHeader.name && oldListHeader.name_v == newListNameV) { if (0 == newListVersion) newListVersion = incrementListVersion(listId, jserver); renameList(listId, newListName, jserver); } //Existing list, lets see if there are any changes required //iterate this client list's items foreach (JObject citem in l["items"]) { string itemId = (string)citem["id"]; int itemPriority = (int)citem["priority"]; string itemText = (string)citem["text"]; bool itemCompleted = (bool)citem["completed"]; //New item and not deleted? if (itemId.StartsWith("NEW_") && itemPriority != -1) { if (0 == newListVersion) newListVersion = incrementListVersion(listId, jserver); addNewListItem(listId, citem, jserver); continue; } //check if deleted if (itemPriority == -1) { if (0 == newListVersion) newListVersion = incrementListVersion(listId, jserver); removeListItem(listId, itemId, jserver); continue; } //check if changed or removed (sitem=null) JObject sitem = getListItem(listId, itemId, jserver); if (sitem != null && ((string)sitem["text"] != itemText || (int)sitem["priority"] != itemPriority || (bool)sitem["completed"] != itemCompleted)) { //replace server item with client item if (0 == newListVersion) newListVersion = incrementListVersion(listId, jserver); //make a replacement item JObject ritem = new JObject(); ritem["id"] = itemId; ritem["completed"] = itemCompleted; ritem["text"] = itemText; ritem["priority"] = itemPriority; ritem["v"] = newListVersion; //replace it replaceListItem(listId, itemId, ritem, jserver); continue; } } } //completed, return the synced list return jserver; } //////////////////////////////////////////////////////////////////////// // SYNC UTILITIES //////////////////////////////////////////////////////////////////////// //////////////////////////////////////////// // Increment a server list's version number // private long incrementListVersion(string listId, JArray jserver) { foreach (JObject slist in jserver) { if (((string)slist["id"]) == listId) { long listversion = (long)slist["v"]; listversion++; slist["v"] = listversion; return listversion; } } throw new System.ArgumentNullException("listId[" + listId + "]", "SyncController::incrementListVersion -> List id not found in server docs lists"); } //////////////////////////////////////////// // Get a list item from the server // private JObject getListItem(string listId, string itemId, JArray jserver) { foreach (JObject slist in jserver) { if (((string)slist["id"]) == listId) { foreach (JObject sitem in slist["items"]) { if ((string)sitem["id"] == itemId) { return sitem; } } } } return null;//not exceptional, client has item that other user deleted //throw new System.ArgumentException("SyncController::getListItem -> List item[" + listId + "][" + itemId + "] not found in servers lists"); } //////////////////////////////////////////// // Get a list "header" from the server // private ListHeaderData getListHeaderData(string listId, JArray jserver) { foreach (JObject slist in jserver) { if (((string)slist["id"]) == listId) { ListHeaderData lnd = new ListHeaderData(); lnd.name = (string)slist["name"]; lnd.name_v = (long)slist["name_v"]; lnd.v = (long)slist["v"]; return lnd; } } //Doesn't exist, probably deleted previously and attempting to sync a deleted list return new ListHeaderData() { v = -999 }; } private class ListHeaderData { public long name_v { get; set; } public string name { get; set; } public long v { get; set; } } //////////////////////////////////////////// //Rename a list // private void renameList(string listId, string newName, JArray jserver) { _logger.LogInformation(LoggingEvents.RenameList, $"Rename list id {listId} to {newName}"); foreach (JObject slist in jserver) { if (((string)slist["id"]) == listId) { slist["name"] = newName; slist["name_v"] = slist["v"]; } } } //////////////////////////////////////////// // Clean up and ID a client list item // private JObject createServerReadyListItemFromClientListItem(JObject citem) { JObject sitem = new JObject(); sitem["id"] = Util.ShortId.Generate(); sitem["completed"] = (bool)citem["completed"]; sitem["text"] = (string)citem["text"]; sitem["v"] = 1; sitem["priority"] = (int)citem["priority"]; return sitem; } //////////////////////////////////////////// //Add a new list item from the client // private void addNewListItem(string listId, JObject citem, JArray jserver) { _logger.LogInformation(LoggingEvents.InsertItem, "addNewListItem"); //NOTE: new list item should have starting version number equal to //server list's master v value JObject sitem = createServerReadyListItemFromClientListItem(citem); foreach (JObject slist in jserver) { if (((string)slist["id"]) == listId) { ((JArray)slist["items"]).Add(sitem); } } } //////////////////////////////////////////// // Replace a list item from the client // private void replaceListItem(string listId, string itemId, JObject citem, JArray jserver) { removeListItem(listId, itemId, jserver); addNewListItem(listId, citem, jserver); } //////////////////////////////////////////// //Remove list item // private void removeListItem(string listId, string listItemId, JArray jserver) { _logger.LogInformation(LoggingEvents.DeleteItem, $"Remove list item - list id {listId} listItemId {listItemId}"); foreach (JObject slist in jserver) { if (((string)slist["id"]) == listId) { int nRemoveAt = -1; JArray sitems = (JArray)slist["items"]; for (int i = 0; i < sitems.Count; i++) { if ((string)sitems[i]["id"] == listItemId) { nRemoveAt = i; break; } } if (nRemoveAt == -1) { _logger.LogWarning(LoggingEvents.DeleteItem, $"Remove list item NOT FOUND - list id {listId} listItemId {listItemId}"); }else{ sitems.RemoveAt(nRemoveAt); } return; } } } /////////////////////////////////////////// //Add a new list from the client // private void addNewList(JObject clist, JArray jserver) { _logger.LogInformation(LoggingEvents.AddList, "Add new list"); //NOTE: New lists start at version 1 for everything JObject slist = new JObject(); //set list headers slist["id"] = Util.ShortId.Generate(); slist["name"] = clist["name"]; slist["name_v"] = 1; slist["v"] = 1; //copy over the list data JArray sitems = new JArray(); foreach (JObject citem in clist["items"]) { sitems.Add(createServerReadyListItemFromClientListItem(citem)); } slist["items"] = sitems; jserver.Add(slist); } //////////////////////////////////////////// // Remove list // private void removeList(string listId, JArray jserver) { _logger.LogInformation(LoggingEvents.DeleteList, $"Remove list {listId}"); int nRemoveAt = -1; for (int i = 0; i < jserver.Count; i++) { if ((string)jserver[i]["id"] == listId) { nRemoveAt = i; break; } } jserver.RemoveAt(nRemoveAt); return; } //----------------- #region LoggingEvents public class LoggingEvents { public const int SyncList = 1000; public const int RenameList = 1001; public const int GetItem = 1002; public const int InsertItem = 1003; public const int UpdateItem = 1004; public const int DeleteItem = 1005; public const int AddList = 1006; public const int DeleteList = 1007; public const int GetItemNotFound = 4000; public const int UpdateItemNotFound = 4001; } #endregion //------------- } }