internal RecipeRecord GetRecord(RecordKey key) { if (key == null) { throw new ArgumentNullException("GetRecord called with null key"); } if (!key.IsDefined) { throw new ArgumentException("GetRecord called with key that is not fully defined"); } return(GetRecord(null, key)); }
public void PutRecord(RecipeRecord record) { if (record.Context != this) { throw new ArgumentException("Cannot add a record from a different context to this context"); } RecordKey key = new RecordKey(record); RecipeRecord check = GetRecord(key); if (check.NrdoId != null && check.NrdoId != record.NrdoId) { throw new ArgumentException("Mismatched nrdo.id values, cannot overwrite nrdo.id='" + check.NrdoId + "' with nrdo.id='" + record.NrdoId + "'"); } if (check.NrdoId == null) { records.Remove(check.InternalId); } recordsByKey[record.TableName][key] = record; records[record.InternalId] = record; }
public override bool Equals(object obj) { RecordKey key = obj as RecordKey; if (key == null) { return(false); } if (key.table.Name != table.Name) { return(false); } foreach (NrdoFieldRef field in table.PkeyGet.Fields) { if (!object.Equals(getValue(field.Field), key.getValue(field.Field))) { return(false); } } return(true); }
internal RecipeRecord GetRecord(string nrdoId, RecordKey key) { RecipeRecord resultById = null; if (nrdoId != null) { if (records.ContainsKey(nrdoId)) { resultById = verifyRecord(records[nrdoId]); } } RecipeRecord resultByKey = null; if (key != null && key.IsDefined) { Dictionary <RecordKey, RecipeRecord> byKey; if (!recordsByKey.ContainsKey(key.Table.Name)) { // This is where we load all the records from the context based on their primary key. // Normally, this is pretty straightforward. However, since adding support for table renaming, it's possible that more than one record can end // up existing with the same primary key - because at some point in the past we had used a new nrdo.id and a find.by to compensate for the // previous lack of renaming support. So there's a record from the old table name with an old nrdo.id and a record from the current table // name with the new nrdo.id and they both are now understood to represent the same record with the same id. Having two records for the same // table with the same primary key but different nrdo.ids is of course forbidden. The way we disambiguate is to look and see if one of them // has the current table name and the other does not - and if so we remove the one with the outdated table name. // There is a theoretical possibility that there could be a double-rename of a table so two records could *both* have outdated table names. // If that happens, the correct solution would be to determine which table name is more recent, but at the moment we do not attempt to do this. // It'd also be possible to attempt to check whether the record in question actually still exists in the DB - and if not, of course, // we can ignore it in the RecipeContext. Again, we don't currently attempt to do this. List <RecipeRecord> recordsToRemove = new List <RecipeRecord>(); byKey = new Dictionary <RecordKey, RecipeRecord>(); foreach (RecipeRecord record in records.Values) { if (record.TableName == key.Table.Name) { var thisRecord = record; var recordKey = new RecordKey(thisRecord); if (byKey.ContainsKey(recordKey)) { var otherRecord = byKey[recordKey]; var thisOneIsCurrent = record.TableName == thisRecord.OriginalTableName; var otherIsCurrent = otherRecord.TableName == otherRecord.OriginalTableName; if (thisOneIsCurrent == otherIsCurrent) { throw new ArgumentException("The recipe context contains the same record key " + recordKey.ToString() + " for two separate records (nrdo.ids " + thisRecord.NrdoId + " and " + otherRecord.NrdoId + ").\r\n" + "This can happen due to the fact that recipes now support table renaming and records with old table names that were being ignored can now pop up and conflict with new records.\r\n" + "However, in this case, we cannot disambiguate based on table name because " + (thisOneIsCurrent ? "both" : "neither") + " of them have the current table name. See the comments in RecipeContext.cs for more details."); } else if (thisOneIsCurrent) { recordsToRemove.Add(otherRecord); } else { recordsToRemove.Add(thisRecord); thisRecord = otherRecord; } } byKey[recordKey] = thisRecord; } } recordsByKey[key.Table.Name] = byKey; foreach (var remove in recordsToRemove) { records.Remove(remove.InternalId); } } byKey = recordsByKey[key.Table.Name]; byKey.TryGetValue(key, out resultByKey); resultByKey = verifyRecord(resultByKey); } // If they both got results and they weren't equal, that's an error if (resultById != null && resultByKey != null && resultById != resultByKey) { throw new ArgumentException("Record found by nrdo.id='" + nrdoId + "' does not match record found by primary key"); } // Getting here means that either one or both is null, or they're equal, so there's // only one possible record to return as the result: RecipeRecord result = resultById ?? resultByKey; // If a result found by key has a nrdoId, but it doesn't match the specified // nrdoId, that's an error if (resultByKey != null && nrdoId != null && resultByKey.NrdoId != null && resultByKey.NrdoId != nrdoId) { throw new ArgumentException("Record in context found by primary key has nrdo.id='" + resultByKey.NrdoId + "', does not match specified nrdo.id='" + nrdoId + "'"); } // If key was given, and a record was found by nrdoId that doesn't // match any of the specified fields of key, that's an error if (resultById != null && key != null) { foreach (NrdoFieldRef field in key.Table.PkeyGet.Fields) { object value = key.GetValue(field.Field); if (!(value is Undefined)) { object actualValue = field.Field.Get(resultById.GetData()); if (!(value is Undefined) && !object.Equals(value, actualValue)) { throw new ArgumentException("Record found in context for nrdo.id='" + nrdoId + "' has value of field " + field.Field.Name + " (" + actualValue + ") that does not match what was specified (" + value + ")"); } } } } // If it couldn't be found in the context it might still be findable in // the database itself... if (result == null && key != null && key.IsDefined) { result = new RecipeRecord(this, key.Table, nrdoId); foreach (NrdoFieldRef field in key.Table.PkeyGet.Fields) { object value = key.GetValue(field.Field); result.PutField(new RecipeValueField(result, field.Field.Name, value)); } if (result.GetData() == null) { result = null; } } return(result); }
internal void Run(RecipeContext context) { DateTime now = DateTime.Now; XmlElement recipeElement = doc.DocumentElement; if (recipeElement.LocalName != "nrdo.recipe") { throw new InvalidDataException("Root element of a recipe must be <nrdo.recipe>"); } foreach (XmlElement recordElement in elementChildren(recipeElement)) { debug(recordElement, "Starting..."); if (recordElement.LocalName == "nrdo.id.changed") { context.ChangeNrdoId(recordElement.GetAttribute("from"), recordElement.GetAttribute("to")); continue; } // Verify that the element corresponds to a table we know about NrdoTable table = NrdoTable.GetTable(context.Lookup, recordElement.LocalName.Replace('.', ':')); if (table == null) { throw new ArgumentException("No table called " + recordElement.LocalName + " could be found."); } // Scan for eligible references on the table, and break any corresponding values down into individual attributes // of the :nrdoid.fieldname variety. // NOTE: It's inefficient to just translate them into :nrdoid.fieldname rather than looking up the value at this // point, because we have the object already and don't need to figure everything out again when we come to parse // the attribute, but since we're storing the values into an XML attribute, we need something we can stringify. // In future we could implement a cache on :nrdoid.fieldname strings, in which case we'd prepopulate that cache // at this point. Recipe loading is not considered perf-critical though. Fortunately! foreach (NrdoReference reference in table.References) { if (isEligibleReference(reference) && recordElement.HasAttribute(reference.Name)) { string targetId = XmlUtil.GetAttr(recordElement, reference.Name); RecipeRecord target = context.GetRecord(targetId); recordElement.RemoveAttribute(reference.Name); string raction = XmlUtil.GetAttr(recordElement, "nrdo.action"); if (target == null && raction != "none" && raction != "delete") { throw new ArgumentException("Could not find record with nrdo.id " + targetId + " in context (processing " + recordElement.LocalName + " " + XmlUtil.GetAttr(recordElement, "nrdo.id") + " " + raction + ")"); } if (target != null) { if (target.TableName != reference.TargetTable.Name) { throw new ArgumentException("Target of reference " + reference.Name + " on " + table.Name + " is " + reference.TargetTable.Name + ", but nrdo.id " + targetId + " refers to a " + target.TableName); } foreach (NrdoJoin join in reference.Joins) { if (recordElement.HasAttribute(join.From.Field.Name)) { throw new ArgumentException("Cannot specify field " + join.From.Field.Name + " at the same time as reference " + reference.Name + " which uses that field"); } recordElement.SetAttribute(join.From.Field.Name, ":" + targetId + "." + join.To.Field.Name); } } } } // Construct a key for looking up the value based on primary key RecordKey key = new RecordKey(table, delegate(NrdoField field) { if (recordElement.HasAttribute(field.Name)) { return(context.evaluate(field.Type, XmlUtil.GetAttr(recordElement, field.Name))); } else { return(Undefined.Value); } }); // Validate the nrdo.id attribute // nrdo.id is required unless the pkey is not sequenced, AND all the // pkey fields are specified (which is equivalent to key.IsDefined) string nrdoId = null; if (recordElement.HasAttribute("nrdo.id")) { nrdoId = XmlUtil.GetAttr(recordElement, "nrdo.id"); if (!Regex.IsMatch(nrdoId, "^[A-Za-z_][A-Za-z0-9_-]*$")) { throw new ArgumentException("Illegal nrdo.id value " + nrdoId); } } else { if (table.IsPkeySequenced || !key.IsDefined) { throw new ArgumentException("Must specify nrdo.id attribute on " + table.Name); } } // Verify that only attributes that are supposed to exist on the element actually do. foreach (XmlAttribute attr in recordElement.Attributes) { switch (attr.Name) { case "nrdo.find.by": case "nrdo.find.where": if (!recordElement.HasAttribute("nrdo.id")) { throw new ArgumentException(attr.Name + " can only be specified if nrdo.id is present on " + table.Name); } if (key.IsDefined) { throw new ArgumentException(attr.Name + " cannot be specified if all primary keys are given on " + table.Name + " (" + recordElement.GetAttribute("nrdo.id") + ")"); } break; case "nrdo.id": case "nrdo.exists": case "nrdo.action": // These are always legal. Nothing to see here. Move along. break; default: if (table.GetField(attr.Name) == null) { throw new ArgumentException("No such field as " + attr.Name + " defined on " + table.Name); } break; } } // Go and look up the record. If found, check its type matches the element, then clone it. ITableObject data; RecipeRecord record = context.GetRecord(nrdoId, key); if (record != null) { if (record.TableName != table.Name) { throw new ArgumentException("nrdo.id " + nrdoId + " refers to a " + record.TableName + " record in the context, but is used on a " + table.Name + " record here."); } data = record.GetData(); record = record.Clone(); } else { record = new RecipeRecord(context, table, nrdoId); data = null; if (recordElement.HasAttribute("nrdo.find.by") && recordElement.HasAttribute("nrdo.find.where")) { throw new ArgumentException("Cannot specify both nrdo.find.by and nrdo.find.where"); } if (recordElement.HasAttribute("nrdo.find.where")) { throw new ArgumentException("nrdo.find.where is not implemented"); } else if (recordElement.HasAttribute("nrdo.find.by")) { NrdoSingleGet get = null; foreach (NrdoGet aGet in table.Gets) { if (aGet is NrdoSingleGet && aGet.Name == XmlUtil.GetAttr(recordElement, "nrdo.find.by")) { get = (NrdoSingleGet)aGet; break; } } if (get == null) { throw new ArgumentException("No single get by " + XmlUtil.GetAttr(recordElement, "nrdo.find.by") + " found on " + table.Name); } data = invokeGet(get, delegate(NrdoField field) { if (recordElement.HasAttribute(field.Name)) { return(context.evaluate(field.NullableType, XmlUtil.GetAttr(recordElement, field.Name))); } else { return(null); } }); } if (data != null) { foreach (NrdoField field in table.Fields) { record.PutField(new RecipeField(record, field.Name)); } foreach (NrdoFieldRef field in table.PkeyGet.Fields) { record.PutField(new RecipeValueField(record, field.Field.Name, field.Field.Get(data))); } } } // Validate against the nrdo.exists attribute, if present. if (recordElement.HasAttribute("nrdo.exists")) { string val = XmlUtil.GetAttr(recordElement, "nrdo.exists"); switch (val) { case "required": if (data == null) { throw new ArgumentException("The record " + nrdoId + " is specified as 'required' in the recipe, but does not exist"); } break; case "permitted": break; case "error": if (data != null) { throw new ArgumentException("The record " + nrdoId + " exists, but the recipe specifies that it must not"); } break; default: throw new ArgumentException("Illegal value " + val + " for nrdo.exists attribute, legal values are required, permitted and error"); } } // Determine what needs to be done based on the nrdo.action attribute string action = "update"; bool setFields = true; bool overwriteFields = false; if (recordElement.HasAttribute("nrdo.action")) { action = XmlUtil.GetAttr(recordElement, "nrdo.action"); } switch (action) { case "none": setFields = false; break; case "ensure": if (data != null) { setFields = false; } // Otherwise equivalent to "update", which is the default case, nothing to do. break; case "update": // Nothing to do; this is the default case. break; case "replace": overwriteFields = true; break; case "delete": if (data != null) { data.Delete(); context.RemoveRecord(record); } continue; default: throw new ArgumentException("Illegal value " + action + " for the nrdo.action attribute, legal values are none, ensure, update, replace and delete"); } // If the record doesn't correspond to an existing item in the database, create it. if (data == null) { // If the record wasn't found AND isn't being created, then we don't need to add it to the database or // to the context. if (!setFields) { continue; } List <object> ctorArgs = new List <object>(); foreach (NrdoField field in table.CtorParams) { object value = Undefined.Value; if (recordElement.HasAttribute(field.Name)) { value = context.evaluate(field.Type, XmlUtil.GetAttr(recordElement, field.Name)); } if (value is Undefined) { value = defaultValue(field.Type, field.IsNullable); } if (value is Now) { ctorArgs.Add(now); record.PutField(new RecipeField(record, field.Name)); } else { ctorArgs.Add(value); record.PutField(new RecipeValueField(record, field.Name, value)); } } data = table.Create(ctorArgs.ToArray()); } // Set the appropriate fields based on what's already known in the context and whether overwriteFields is true foreach (NrdoField field in table.Fields) { // Figure out whether this particular field should get set bool setIt = false; if (field.IsWritable && recordElement.HasAttribute(field.Name)) { if (data.IsNew || overwriteFields) { setIt = setFields; } else { RecipeField rfield = record.GetField(field.Name); if (rfield == null) { // Wish I could figure out something smarter to do here setIt = false; } else if (rfield is RecipeValueField) { object value = ((RecipeValueField)rfield).Value; if (object.Equals(field.Get(data), value)) { setIt = setFields; } } } } // Set the field if (setIt) { object value = context.evaluate(field.Type, XmlUtil.GetAttr(recordElement, field.Name)); if (value is Now) { field.Set(data, now); record.PutField(new RecipeField(record, field.Name)); } else { field.Set(data, value); record.PutField(new RecipeValueField(record, field.Name, value)); } } // Save the fact that we know the field, but not the value. But not if it's a readonly field, // because that would have been set when the ctor was being processed. else if (record.GetField(field.Name) == null) { // If the record was newly created, we know the value is the default for the type. Otherwise, // we store that we just don't know the value. if (data.IsNew) { if (field.IsWritable) { record.PutField(new RecipeValueField(record, field.Name, defaultValue(field.Type, field.IsNullable))); } else { // must either have been picked up by the constructor, or // be the sequenced pkey and will be picked up below } } else { record.PutField(new RecipeField(record, field.Name)); } } } // Save the newly created object into the database and into the context if (setFields) { data.Update(); } // This covers sequenced pkeys and also covers the case where // a DateTime field in the pkey was set to :now. If it's part of // the pkey we can't change it (so storing the value is harmless) // but we need to know it for future reference. foreach (NrdoFieldRef pkeyField in table.PkeyGet.Fields) { record.PutField(new RecipeValueField(record, pkeyField.Field.Name, pkeyField.Field.Get(data))); } context.PutRecord(record); debug(recordElement, "Finished"); } }