This commit is contained in:
456
Controllers/SyncController.cs
Normal file
456
Controllers/SyncController.cs
Normal file
@@ -0,0 +1,456 @@
|
||||
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<SyncController> 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
|
||||
|
||||
//-------------
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user