Example #1
0
        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);
        }
Example #2
0
 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));
         }
     };
 }
Example #3
0
 public RecordKey(NrdoTable table, FieldValueGetter getValue)
 {
     this.table    = table;
     this.getValue = getValue;
 }
Example #4
0
        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);
        }
Example #5
0
        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"));
                }
            }
        }
Example #6
0
File: Recipe.cs Project: sab39/nrdo
        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");
            }
        }
Example #7
0
 public RecipeRecord(RecipeContext context, NrdoTable table, string nrdoId)
     : this(context, table.Name, table.Name, nrdoId)
 {
     this.table = table;
 }