348 lines
14 KiB
C#
348 lines
14 KiB
C#
using System.Threading.Tasks;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Sockeye.Util;
|
|
using Sockeye.Api.ControllerHelpers;
|
|
using Sockeye.Models;
|
|
using System.Collections.Generic;
|
|
using System;
|
|
using System.Linq;
|
|
|
|
|
|
namespace Sockeye.Biz
|
|
{
|
|
|
|
internal class TagBiz : BizObject//, IJobObject
|
|
{
|
|
internal TagBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles)
|
|
{
|
|
ct = dbcontext;
|
|
UserId = currentUserId;
|
|
UserTranslationId = userTranslationId;
|
|
CurrentUserRoles = UserRoles;
|
|
BizType = SockType.NoType;
|
|
}
|
|
|
|
internal static TagBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
|
|
{
|
|
if (httpContext != null)
|
|
return new TagBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
|
|
else
|
|
return new TagBiz(ct, 1, ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin);
|
|
}
|
|
|
|
|
|
#region Utilities
|
|
/////////////////////////////////////
|
|
//UTILITIES
|
|
//
|
|
|
|
//clean up tags from client submission
|
|
//remove dupes, substitute dashes for spaces, lowercase and shorten if exceed 255 chars
|
|
//and sorts before returning to ensure consistent ordering
|
|
public static List<string> NormalizeTags(List<string> inTags)
|
|
{
|
|
if (inTags == null || inTags.Count == 0) return inTags;
|
|
|
|
List<string> outTags = new List<string>();
|
|
foreach (var tag in inTags)
|
|
outTags.Add(NormalizeTag(tag));
|
|
outTags.Sort();
|
|
return outTags.Distinct().ToList();
|
|
}
|
|
|
|
|
|
public static string NormalizeTag(string inObj)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(inObj)) return null;
|
|
//Must be lowercase per rules
|
|
//This may be naive when we get international cust omers but for now supporting utf-8 and it appears it's safe to do this with unicode
|
|
inObj = inObj.ToLower(System.Globalization.CultureInfo.CurrentCulture);
|
|
//No spaces in tags, replace with dashes
|
|
inObj = inObj.Replace(" ", "-");
|
|
//Remove multiple dash sequences
|
|
inObj = System.Text.RegularExpressions.Regex.Replace(inObj, "-+", "-");
|
|
//Ensure doesn't start or end with a dash
|
|
inObj = inObj.Trim('-');
|
|
//No longer than 255 characters
|
|
inObj = StringUtil.MaxLength(inObj, 255);
|
|
return inObj;
|
|
}
|
|
|
|
|
|
|
|
public static async Task ProcessDeleteTagsInRepositoryAsync(AyContext ct, List<string> deleteTags)
|
|
{
|
|
if (deleteTags.Count == 0) return;
|
|
|
|
var existing = await ct.Tag.Where(x => deleteTags.Contains(x.Name)).ToListAsync();
|
|
foreach (string s in deleteTags)
|
|
{
|
|
var t = existing.FirstOrDefault(x => x.Name == s);
|
|
if (t != null)
|
|
{
|
|
if (t.RefCount < 2)//catch any that fell through the cracks and are maybe zero or negative event
|
|
ct.Remove(t);
|
|
else
|
|
t.RefCount -= 1;
|
|
}
|
|
|
|
}
|
|
await ct.SaveChangesAsync();
|
|
|
|
#region OLD SLOW METHOD FOR REFERENCE IN CASE CONCURRENCY EXCEPTIONS
|
|
// foreach (string s in deleteTags)
|
|
// {
|
|
// bool bDone = false;
|
|
// //Keep on trying until there is success
|
|
// //this allows for concurrency issues
|
|
// //I considered a circuit breaker / timeout here, but theoretically it should not be an issue
|
|
// //at some point it should not be a concurrency issue anymore
|
|
// //And this is not critical functionality requiring a transaction and locking
|
|
// do
|
|
// {
|
|
// //START: Get tag word and concurrency token and count
|
|
// var ExistingTag = await ct.Tag.FirstOrDefaultAsync(z => z.Name == s);
|
|
// //if not present, then nothing to do
|
|
// if (ExistingTag != null)
|
|
// {
|
|
// //No longer needed?
|
|
// if (ExistingTag.RefCount == 1)
|
|
// {
|
|
// ct.Remove(ExistingTag);
|
|
|
|
// }
|
|
// else
|
|
// {
|
|
// //Decrement the refcount
|
|
// ExistingTag.RefCount -= 1;
|
|
// }
|
|
|
|
// try
|
|
// {
|
|
// await ct.SaveChangesAsync();
|
|
// bDone = true;
|
|
// }
|
|
// catch (Exception ex) when (ex is DbUpdateConcurrencyException)//allow for possible other types
|
|
// {
|
|
// //allow others to flow past
|
|
// // string sss = ex.ToString();
|
|
// }
|
|
// }
|
|
// else
|
|
// {
|
|
// bDone = true;
|
|
// }
|
|
// } while (bDone == false);
|
|
// }
|
|
#endregion old slow method
|
|
|
|
|
|
}
|
|
|
|
public static async Task ProcessUpdateTagsInRepositoryAsync(AyContext ct, List<string> newTags, List<string> originalTags = null)
|
|
{
|
|
//todo: Recode this as a procedure like search indexing or at least a direct sql call
|
|
|
|
|
|
//just in case no new tags are present which could mean a user removed all tags from a record so this
|
|
//needs to proceed with the code below even if newTags is null as long as originalTags isn't also null
|
|
if (newTags == null) newTags = new List<string>();
|
|
if (originalTags == null) originalTags = new List<string>();
|
|
|
|
if (newTags.Count == 0 && originalTags.Count == 0) return;
|
|
|
|
List<string> deleteTags = new List<string>();
|
|
List<string> addTags = new List<string>();
|
|
|
|
if (originalTags != null)
|
|
{
|
|
//Update
|
|
//This logic to only come up with CHANGES, if the item is in both lists then it will disappear and not need to be dealt with as it's refcount is unchanged in this operation
|
|
//testing will validate it
|
|
deleteTags = originalTags.Except(newTags).ToList();
|
|
addTags = newTags.Except(originalTags).ToList();
|
|
}
|
|
else
|
|
{
|
|
//Add
|
|
addTags = newTags;
|
|
}
|
|
|
|
#region OLD SLOW METHOD FOR REFERENCE IN CASE CONCURRENCY EXCEPTIONS
|
|
// //ADD / INCREMENT TAGS
|
|
// //one by one method
|
|
// foreach (string s in addTags)
|
|
// {
|
|
// bool bDone = false;
|
|
// //Keep on trying until there is success
|
|
// //this allows for concurrency issues
|
|
// //I considered a circuit breaker / timeout here, but theoretically it should not be an issue
|
|
// //at some point it should not be a concurrency issue anymore
|
|
// do
|
|
// {
|
|
// //START: Get tag word and concurrency token and count
|
|
// var ExistingTag = await ct.Tag.FirstOrDefaultAsync(z => z.Name == s);
|
|
// //if not present, then add it with a count of 0
|
|
// if (ExistingTag == null)
|
|
// {
|
|
// await ct.Tag.AddAsync(new Tag() { Name = s, RefCount = 1 });
|
|
// }
|
|
// else
|
|
// {
|
|
// //Update the refcount
|
|
// ExistingTag.RefCount += 1;
|
|
// }
|
|
// try
|
|
// {
|
|
// await ct.SaveChangesAsync();
|
|
// bDone = true;
|
|
// }
|
|
// catch (Exception ex) when (ex is DbUpdateConcurrencyException)//this allows for other types
|
|
// {
|
|
|
|
// Console.WriteLine("TagBiz::Exception udring update tags");
|
|
// //allow others to flow past
|
|
// //string sss = ex.ToString();
|
|
// }
|
|
// } while (bDone == false);
|
|
// }
|
|
|
|
#endregion old slow method
|
|
|
|
//ADD / INCREMENT TAGS
|
|
var existing = await ct.Tag.Where(x => addTags.Contains(x.Name)).ToListAsync();
|
|
foreach (string s in addTags)
|
|
{
|
|
var t = existing.FirstOrDefault(x => x.Name == s);
|
|
if (t != null)
|
|
{
|
|
t.RefCount += 1;
|
|
}
|
|
else
|
|
{
|
|
ct.Tag.Add(new Tag() { Name = s, RefCount = 1 });
|
|
}
|
|
}
|
|
await ct.SaveChangesAsync();
|
|
|
|
|
|
|
|
//DELETE TAGS
|
|
await ProcessDeleteTagsInRepositoryAsync(ct, deleteTags);
|
|
}
|
|
|
|
|
|
|
|
//Pick list for driving pick list route
|
|
//going with contains for now as I think it's more useful in the long run and still captures startswith intent by user
|
|
public static async Task<List<string>> TagListFilteredAsync(AyContext ct, string q)
|
|
{
|
|
//This path is intended for internal use and accepts that there may not be a filter specified
|
|
//however the client will always require a filter to display a tag list for choosing from
|
|
if (string.IsNullOrWhiteSpace(q))
|
|
{
|
|
return await ct.Tag.OrderBy(z => z.Name)
|
|
.Select(z => z.Name)
|
|
.AsNoTracking()
|
|
.ToListAsync();
|
|
}
|
|
else
|
|
{
|
|
q = NormalizeTag(q);
|
|
return await ct.Tag
|
|
.Where(z => z.Name.Contains(q))
|
|
.OrderBy(z => z.Name)
|
|
.Select(z => z.Name)
|
|
.Take(25)
|
|
.AsNoTracking()
|
|
.ToListAsync();
|
|
}
|
|
}
|
|
|
|
//Cloud list
|
|
public static async Task<List<TagCloudItem>> CloudListFilteredAsync(AyContext ct, string q)
|
|
{
|
|
//This path is intended for internal use and accepts that there may not be a filter specified
|
|
//however the client will always require a filter to display a tag list for choosing from
|
|
if (string.IsNullOrWhiteSpace(q))
|
|
{
|
|
return await ct.Tag.OrderByDescending(z => z.RefCount).Select(z => new TagCloudItem() { Name = z.Name, RefCount = z.RefCount }).AsNoTracking().ToListAsync();
|
|
}
|
|
else
|
|
{
|
|
q = NormalizeTag(q);
|
|
//TODO: Use the EF CORE TAKE method to restrict the results to a maximum limit
|
|
//however need to ensure it doesn't balk when the limit is higher than the number of results (probably not but test that)
|
|
return await ct.Tag
|
|
.Where(z => z.Name.Contains(q))
|
|
.OrderByDescending(z => z.RefCount)
|
|
.Select(z => new TagCloudItem() { Name = z.Name, RefCount = z.RefCount })
|
|
.AsNoTracking()
|
|
.ToListAsync();
|
|
}
|
|
}
|
|
public class TagCloudItem
|
|
{
|
|
public long RefCount { get; set; }
|
|
public string Name { get; set; }
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Process batch tag operation
|
|
/// </summary>
|
|
/// <returns>true if object needs to be saved or false if no changes were made</returns>
|
|
internal static bool ProcessBatchTagOperation(List<string> tagCollection, string tag, string toTag, JobSubType subType)
|
|
{
|
|
switch (subType)
|
|
{
|
|
case JobSubType.TagAddAny:
|
|
case JobSubType.TagAdd:
|
|
if (!tagCollection.Contains(tag))
|
|
{
|
|
tagCollection.Add(tag);
|
|
return true;
|
|
}
|
|
return false;
|
|
case JobSubType.TagRemoveAny:
|
|
case JobSubType.TagRemove:
|
|
return tagCollection.Remove(tag);
|
|
case JobSubType.TagReplaceAny:
|
|
case JobSubType.TagReplace:
|
|
int index = tagCollection.IndexOf(tag);
|
|
if (index != -1)
|
|
{
|
|
tagCollection[index] = toTag;
|
|
return true;
|
|
}
|
|
return false;
|
|
default:
|
|
throw new System.ArgumentOutOfRangeException($"ProcessBatchTagOperation -> Invalid job Subtype{subType}");
|
|
}
|
|
}
|
|
|
|
|
|
#endregion utilities
|
|
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//JOB / OPERATIONS
|
|
//
|
|
|
|
|
|
|
|
//Other job handlers here...
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////
|
|
|
|
}//eoc
|
|
|
|
|
|
}//eons
|
|
|