private XmlElement CanonicalizeElement(XmlElement element, NrdoTable type, XmlElement beforeElement) { element.ParentNode.RemoveChild(element); XmlElement newElement = element.OwnerDocument.CreateElement(type.Name.Replace(':', '.')); foreach (XmlAttribute attr in element.Attributes) { newElement.SetAttribute(attr.Name, attr.Value); } beforeElement.ParentNode.InsertBefore(newElement, beforeElement); CanonicalizeChildren(element, newElement, type, newElement, beforeElement); return(newElement); }
public RecordKey(ITableObject data) { this.table = NrdoTable.GetTable(data.GetType()); this.getValue = delegate(NrdoField field) { if (data.IsNew && table.IsPkeySequenced && field == table.PkeyGet.Fields[0].Field) { return(Undefined.Value); } else { return(field.Get(data)); } }; }
public RecordKey(NrdoTable table, FieldValueGetter getValue) { this.table = table; this.getValue = getValue; }
internal XmlDocument Canonicalize(XmlDocument doc) { List <ITransformRecipe> transformQueue = new List <ITransformRecipe>(Transforms); debugCounter++; bool again = true; int pass = 0; while (again) { pass++; debugDump(doc, "recipe-" + debugCounter + "-pass-" + pass + "-in"); List <ITransformRecipe> newTransforms = new List <ITransformRecipe>(); XmlElement recipeElement = doc.DocumentElement; if (recipeElement.Name != "nrdo.recipe") { throw new InvalidDataException("Root element of a recipe must be <nrdo.recipe>"); } foreach (XmlElement childElement in new List <XmlElement>(Recipe.elementChildren(recipeElement))) { if (childElement.LocalName.StartsWith("nrdo.")) { if (childElement.LocalName == "nrdo.transform") { recipeElement.RemoveChild(childElement); string type = XmlUtil.GetAttr(childElement, "type") ?? "xslt"; if (!transformTypes.ContainsKey(type)) { throw new ArgumentException("Unknown transform type '" + type + "'"); } ITransformRecipe transform = (ITransformRecipe)Activator.CreateInstance(transformTypes[type]); if (childElement.HasAttribute("src")) { transform.SourceFile = XmlUtil.GetAttr(childElement, "src"); } newTransforms.Add(transform); } } else { NrdoTable table = NrdoTable.GetTable(Lookup, childElement.LocalName.Replace('.', ':')); if (table == null) { throw new ArgumentException("Table " + childElement.LocalName + " could not be found."); } // This is something of a hack - insert a dummy element after the real element that's // going to be removed as part of the canonicalization, for // content to be inserted before. But it works and saves having to // come up with a probably more complicated and hacky robust way to // specify a location in the document that may NOT correspond to any // actual element. XmlElement dummy = doc.CreateElement("dummy"); recipeElement.InsertAfter(dummy, childElement); CanonicalizeElement(childElement, table, dummy); recipeElement.RemoveChild(dummy); } } transformQueue.InsertRange(0, newTransforms); debugDump(doc, "recipe-" + debugCounter + "-pass-" + pass + "-out"); if (transformQueue.Count == 0) { again = false; } else { doc = transformQueue[0].Transform(doc); transformQueue.RemoveAt(0); } } return(doc); }
private void CanonicalizeChildren(XmlElement origElement, XmlElement newElement, NrdoTable type, XmlElement beforeElement, XmlElement afterElement) { XmlElement preFind = null; bool parentIsDelete = XmlUtil.GetAttr(newElement, "nrdo.action") == "delete"; foreach (XmlElement childElement in new List <XmlElement>(Recipe.elementChildren(origElement))) { string name = childElement.LocalName; string action = XmlUtil.GetAttr(childElement, "nrdo.action"); // If it names a field whose type is string, grab either the text or the InnerXml of it (depending on the // "escaped" attribute) and put it in an attribute on newElement, erroring if such attribute already exists. NrdoField field = type.GetField(name); if (field != null && field.Type.Equals(typeof(string))) { if (newElement.HasAttribute(field.Name)) { throw new ArgumentException("Cannot specify " + field.Name + " as both attribute and nested element on " + type.Name); } bool escaped; if (childElement.HasAttribute("escaped")) { switch (XmlUtil.GetAttr(childElement, "escaped")) { case "true": escaped = true; break; case "false": escaped = false; break; default: throw new ArgumentException("escaped attribute must have value 'true' or 'false', not " + XmlUtil.GetAttr(childElement, "escaped")); } } else { escaped = false; } // FIXME: should check for any elements here if (escaped), they're illegal and should be thrown out string value = escaped ? childElement.InnerText : childElement.InnerXml; newElement.SetAttribute(field.Name, value); if (preFind != null) { preFind.SetAttribute(field.Name, value); } continue; } // If it names an eligible reference, then // - CanonicalizeElement(childElement, targetType, beforeElement) and save the result. // - If the result doesn't have a nrdo.id, add one from origElement (or error if it doesn't have one) // - Add a reference-named attribute to newElement with the nrdo.id of the new element. NrdoSingleReference reference = null; foreach (NrdoReference testRef in type.References) { if (testRef.Name == name && Recipe.isEligibleReference(testRef) && testRef.AssociatedGet == testRef.TargetTable.PkeyGet && testRef.TargetTable.IsPkeySequenced) { reference = (NrdoSingleReference)testRef; break; } } if (reference != null) { // If the parent element is being deleted, the child element MUST be deleted too. if (parentIsDelete) { if (action != "none" && action != "delete") { throw new ApplicationException(childElement.LocalName + " element inside deleted " + newElement.LocalName + " element must also have nrdo.action='delete'"); } } else if (action == "delete") { throw new ApplicationException(childElement.LocalName + " element cannot have nrdo.action='delete' because containing " + newElement.LocalName + " element doesn't"); } if (preFind == null) { preFind = newElement.OwnerDocument.CreateElement(newElement.LocalName); foreach (XmlAttribute attr in newElement.Attributes) { preFind.SetAttribute(attr.Name, attr.Value); } if (!parentIsDelete) { preFind.SetAttribute("nrdo.action", "none"); } bool insertPreFind = true; if (!preFind.HasAttribute("nrdo.id")) { foreach (NrdoFieldRef pkeyField in type.PkeyGet.Fields) { if (!preFind.HasAttribute(pkeyField.Field.Name)) { insertPreFind = false; } } } if (insertPreFind) { beforeElement.ParentNode.InsertBefore(preFind, beforeElement); } else if (parentIsDelete) { throw new ApplicationException("Cannot construct correct order for deleting " + childElement.LocalName + " without a nrdo.id, either add the right nrdo.id or reorder the elements manually"); } } if (!childElement.HasAttribute("nrdo.id")) { if (!newElement.HasAttribute("nrdo.id")) { throw new ArgumentException(childElement.Name + " element must have a nrdo.id attribute."); } childElement.SetAttribute("nrdo.id", XmlUtil.GetAttr(newElement, "nrdo.id") + "_" + reference.Name); } // This needs to be done before canonicalizing, because the canonicalization can actually // result in two records (the child may itself end up turning into a 'preFind') and only // one of these gets returned. if (newElement.HasAttribute("nrdo.id")) { childElement.SetAttribute(reference.Joins[0].To.Field.Name, ":" + XmlUtil.GetAttr(newElement, "nrdo.id") + "." + reference.Joins[0].From.Field.Name); } XmlElement result = CanonicalizeElement(childElement, reference.TargetTable, beforeElement); if (newElement.HasAttribute(reference.Name)) { throw new ArgumentException("Cannot specify " + reference.Name + " as both attribute and nested element on " + type.Name); } newElement.SetAttribute(reference.Name, XmlUtil.GetAttr(result, "nrdo.id")); continue; } // Otherwise it should name a table. If it contains a ".", get the table outright. Otherwise start from // the module that "type" is in, trying NrdoTable.GetTable on each until something is found. If nothing is, it's // an error. NrdoTable table = null; if (name.IndexOf('.') >= 0) { table = NrdoTable.GetTable(Lookup, name.Replace('.', ':')); } else { string module = type.Module; while (module != null && table == null) { table = NrdoTable.GetTable(Lookup, module + ":" + name); int pos = module.LastIndexOf(':'); module = pos < 0 ? null : module.Substring(0, pos); } if (table == null) { table = NrdoTable.GetTable(Lookup, name); } } if (table == null) { throw new ArgumentException("No table called " + name + " found from " + type.Module); } // Find all eligible references from that table to "type". If there's not exactly one, error. Otherwise take that // reference. List <NrdoSingleReference> refs = new List <NrdoSingleReference>(); foreach (NrdoReference testRef in table.References) { if (Recipe.isEligibleReference(testRef) && testRef.TargetTable.Name == type.Name) { refs.Add((NrdoSingleReference)testRef); } } if (refs.Count != 1) { throw new ArgumentException("There isn't a single eligible reference from " + table.Name + " to " + type.Name); } if (parentIsDelete && action != "none" && action != "delete") { throw new ApplicationException(childElement.LocalName + " element inside deleted " + newElement.LocalName + " element must also have nrdo.action='delete'"); } else if (parentIsDelete && action == "delete") { // FIXME: this needs to somehow turn into <outer nrdo.action="none"/><inner nrdo.action="delete"/><outer nrdo.action="delete"/> throw new ArgumentException("Not yet implemented: cannot use 'back-references as nested element' construct between two elements that are both being deleted (" + newElement.LocalName + " and " + childElement.LocalName + ")"); } else { // CanonicalizeElement(childElement, tableType, afterElement) and save the result. // Add a reference-named attribute to the result with the nrdo.id of the current element if (!newElement.HasAttribute("nrdo.id")) { throw new ArgumentException("Cannot use 'back-references as nested element' construct on a " + type.Name + " element without a nrdo.id"); } XmlElement refResult = CanonicalizeElement(childElement, table, afterElement); if (refResult.HasAttribute(refs[0].Name)) { throw new ArgumentException("Cannot specify " + refs[0].Name + " attribute directly on " + name + " when nesting inside a " + type.Name); } refResult.SetAttribute(refs[0].Name, XmlUtil.GetAttr(newElement, "nrdo.id")); } } }
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"); } }
public RecipeRecord(RecipeContext context, NrdoTable table, string nrdoId) : this(context, table.Name, table.Name, nrdoId) { this.table = table; }