using Newtonsoft.Json.Linq; using OI.Metrology.Shared.DataModels; using OI.Metrology.Shared.Repositories; using OI.Metrology.Shared.Services; using System; using System.Collections.Generic; using System.Linq; namespace OI.Metrology.Viewer.Services; public class InboundDataService : IInboundDataService { private readonly IMetrologyRepo _Repo; public InboundDataService(IMetrologyRepo repo) => _Repo = repo; public long DoSQLInsert(JToken jsonbody, ToolType toolType, List metaData) { JArray detailsArray = null; string uniqueId = ""; foreach (JToken jt in jsonbody.Children()) { if (jt is JProperty jp) { if (string.Equals(jp.Name, "Details", StringComparison.OrdinalIgnoreCase)) { if (jp.First is JArray array) detailsArray = array; } else if (string.Equals(jp.Name, "UniqueId", StringComparison.OrdinalIgnoreCase)) { uniqueId = Convert.ToString(((JValue)jp.Value).Value); } } } long headerId = 0; using (System.Transactions.TransactionScope transScope = _Repo.StartTransaction()) { try { _Repo.PurgeExistingData(toolType.ID, uniqueId); } catch (Exception ex) { throw new Exception("Failed to purge existing data: " + ex.Message, ex); } try { headerId = _Repo.InsertToolDataJSON(jsonbody, -1, metaData, toolType.HeaderTableName); } catch (Exception ex) { throw new Exception("Insert failed for header: " + ex.Message, ex); } int detailrow = 1; try { if (detailsArray != null) { foreach (JToken detail in detailsArray) { _ = _Repo.InsertToolDataJSON(detail, headerId, metaData, toolType.DataTableName); detailrow += 1; } } } catch (Exception ex) { throw new Exception("Insert failed for detail row " + detailrow.ToString() + ": " + ex.Message, ex); } transScope.Complete(); } return headerId; } // this method is for validating the json contents of the inbound request, it will make sure all required fields are included // errors are generated for missing fields, and warnings are generated for additional fields not in the metadata // this is recursive, detailIndex = 0 is for the header, then it calls itself for each of the details rows (in any) public void ValidateJSONFields(JToken jsonbody, int detailIndex, List metaData, List errors, List warnings) { bool isHeader = detailIndex == 0; string rowDesc = isHeader ? "header" : "detail index " + detailIndex.ToString(); // human readable description for error messages // filter the metadata list by header/detail List fields = metaData.Where(md => md.Header == isHeader).ToList(); // get list of ApiFields from the metadata, exclude blank ApiName List apiFields = fields.Where(f => string.IsNullOrWhiteSpace(f.ApiName) == false).Select(f => f.ApiName.Trim().ToUpper()).ToList(); // get list of ApiFields from the metadata with blank ColumnName - we ignore these fields in the jsonbody List ignoreFields = fields.Where(f => (string.IsNullOrWhiteSpace(f.ApiName) == false) && string.IsNullOrWhiteSpace(f.ColumnName)).Select(f => f.ApiName.Trim().ToUpper()).ToList(); // keep a list of valid fields found in the jsonbody so we can check for duplicates List validFields = new(); // get list of container fields in the ApiFields, ex: Points\Thickness will add Points to the list List containerFields = apiFields.Where(f => f.Contains('\\')).Select(f => f.Split('\\')[0].Trim().ToUpper()).Distinct().ToList(); // pointer to the Details array from the jsonbody, this is hard-coded as the subfield for the common Header/Detail structure JArray detailsArray = null; // process fields in the json body foreach (JToken jt in jsonbody.Children()) { if (jt is JProperty jp) { string jpName = jp.Name.Trim().ToUpper(); if (apiFields.Contains(jpName)) { // Normal field detected, remove it from the list so we know which fields are missing _ = apiFields.Remove(jpName); // Check for duplicates if (validFields.Contains(jpName)) errors.Add("Duplicated field on " + rowDesc + ": " + jp.Name); else validFields.Add(jpName); } else if (string.Equals(jp.Name, "Details", StringComparison.OrdinalIgnoreCase)) { // Details container field found if (!isHeader) errors.Add("Details field not expected on " + rowDesc); if (jp.First is JArray array) detailsArray = array; else if ((jp.First is JValue value) && (value.Value == null)) detailsArray = null; else errors.Add("Invalid details field"); } else if (ignoreFields.Contains(jpName)) { // ignore these fields } else if (containerFields.Contains(jpName)) { // ignore fields that are container fields } else { // output warnings if extra fields are found warnings.Add("Extra field on " + rowDesc + ": " + jp.Name); } } } // process container fields, ex: Points ValidateJSONContainerFields(jsonbody, rowDesc, apiFields, containerFields, errors, warnings); if (containerFields.Count > 1) errors.Add("Only one container field is supported"); if (isHeader && (containerFields.Count > 0)) errors.Add("Container field is only allowed on detail"); // output errors for fields that were not found in the json foreach (string f in apiFields) { errors.Add("Missing field on " + rowDesc + ": " + f); } // if a Details container if found, process it by recursion if (detailsArray != null) { int i = 1; foreach (JToken detail in detailsArray) { ValidateJSONFields(detail, i, metaData, errors, warnings); i += 1; } } } // this method is for validating the special container fields (only used for stratus biorad) // the container fields are used to collapse a 3 tier structure into the 2 tier used in sharepoint protected void ValidateJSONContainerFields(JToken jsonbody, string rowDesc, List apiFields, List containerFields, List errors, List warnings) { // process container fields, ex: Points foreach (string containerField in containerFields) { // get the json data for this container field, ex: Points JProperty contJP = jsonbody.Children().Where(jp => string.Equals(jp.Name, containerField, StringComparison.OrdinalIgnoreCase)).SingleOrDefault(); if ((contJP != null) && (contJP.Value is JArray array)) { JArray contJPArray = array; // Get a list of properties in the container field from the json body, but pre-pend the container field name, ex: Points\Position List contFieldProperties = new(); foreach (JToken sfJT in contJPArray.Values()) // for each row in the json container { foreach (JProperty subJTJP in sfJT.Children()) // for each property for the row { string propname = (containerField + '\\' + subJTJP.Name).ToUpper(); if (!contFieldProperties.Contains(propname)) contFieldProperties.Add(propname); } } // Get list of field bindings for this container field, ex: Points\Position, Points\Thickness List contFieldBindings = apiFields.Where(f => f.StartsWith(containerField, StringComparison.OrdinalIgnoreCase)).Select(f => f.ToUpper()).ToList(); foreach (string contFieldBinding in contFieldBindings) { // check if the jsonbody has this property in the container field if (contFieldProperties.Contains(contFieldBinding)) { _ = contFieldProperties.Remove(contFieldBinding); // remove from the list of properties so we know it was found _ = apiFields.Remove(contFieldBinding); // remove from the list of missing fields } else { errors.Add("Missing field on " + rowDesc + ": " + contFieldBinding); } } // Output warnings for extra properties in the container field foreach (string contFieldProp in contFieldProperties) { warnings.Add("Extra field on " + rowDesc + ": " + contFieldProperties); } } else { errors.Add("Missing container field on " + rowDesc + ": " + containerField); } } } }