Ejemplo n.º 1
0
        /// <summary>
        /// Pass an XML document string in for processing.
        /// </summary>
        /// <param name="stringName">A human-readable identifier useful for debugging. Generally, the name of the file that the string was read from. Not required (but very useful.)</param>
        public void AddString(string input, string stringName = "(unnamed)")
        {
            // This is a really easy error to make; we might as well handle it.
            if (input.EndsWith(".xml"))
            {
                Dbg.Err($"It looks like you've passed the filename {input} to AddString instead of the actual XML file. Either use AddFile() or pass the file contents in.");
            }

            if (s_Status != Status.Accumulating)
            {
                Dbg.Err($"Adding data while while the world is in {s_Status} state; should be {Status.Accumulating} state");
            }

            XDocument doc;

            try
            {
                doc = XDocument.Parse(input, LoadOptions.SetLineInfo);
            }
            catch (System.Xml.XmlException e)
            {
                Dbg.Ex(e);
                return;
            }

            if (doc.Elements().Count() > 1)
            {
                // This isn't testable, unfortunately; XDocument doesn't even support multiple root elements.
                Dbg.Err($"{stringName}: Found {doc.Elements().Count()} root elements instead of the expected 1");
            }

            var readerContext = new ReaderContext(stringName, false);

            foreach (var rootElement in doc.Elements())
            {
                if (rootElement.Name.LocalName != "Decs")
                {
                    Dbg.Wrn($"{stringName}:{rootElement.LineNumber()}: Found root element with name \"{rootElement.Name.LocalName}\" when it should be \"Decs\"");
                }

                foreach (var decElement in rootElement.Elements())
                {
                    string typeName = decElement.Name.LocalName;

                    Type typeHandle = UtilType.ParseDecFormatted(typeName, stringName, decElement.LineNumber());
                    if (typeHandle == null || !typeof(Dec).IsAssignableFrom(typeHandle))
                    {
                        Dbg.Err($"{stringName}:{decElement.LineNumber()}: {typeName} is not a valid root Dec type");
                        continue;
                    }

                    if (decElement.Attribute("decName") == null)
                    {
                        Dbg.Err($"{stringName}:{decElement.LineNumber()}: No dec name provided");
                        continue;
                    }

                    string decName = decElement.Attribute("decName").Value;
                    if (!DecNameValidator.IsMatch(decName))
                    {
                        // This feels very hardcoded, but these are also *by far* the most common errors I've seen, and I haven't come up with a better and more general solution
                        if (decName.Contains(" "))
                        {
                            Dbg.Err($"{stringName}:{decElement.LineNumber()}: Dec name \"{decName}\" is not a valid identifier; consider removing spaces");
                        }
                        else if (decName.Contains("\""))
                        {
                            Dbg.Err($"{stringName}:{decElement.LineNumber()}: Dec name \"{decName}\" is not a valid identifier; consider removing quotes");
                        }
                        else
                        {
                            Dbg.Err($"{stringName}:{decElement.LineNumber()}: Dec name \"{decName}\" is not a valid identifier; dec identifiers must be valid C# identifiers");
                        }

                        continue;
                    }

                    // Consume decName so we know it's not hanging around
                    decElement.Attribute("decName").Remove();

                    // Check to see if we're abstract
                    bool abstrct = false;
                    {
                        var abstractAttribute = decElement.Attribute("abstract");
                        if (abstractAttribute != null)
                        {
                            if (!bool.TryParse(abstractAttribute.Value, out abstrct))
                            {
                                Dbg.Err($"{stringName}:{decElement.LineNumber()}: Error encountered when parsing abstract attribute");
                            }

                            abstractAttribute.Remove();
                        }
                    }

                    // Get our parent info
                    string parent = null;
                    {
                        var parentAttribute = decElement.Attribute("parent");
                        if (parentAttribute != null)
                        {
                            parent = parentAttribute.Value;

                            parentAttribute.Remove();
                        }
                    }

                    // Register ourselves as an available parenting object
                    {
                        var identifier = Tuple.Create(typeHandle.GetDecRootType(), decName);
                        if (potentialParents.ContainsKey(identifier))
                        {
                            Dbg.Err($"{stringName}:{decElement.LineNumber()}: Dec {identifier.Item1}:{identifier.Item2} defined twice");
                        }
                        else
                        {
                            potentialParents[identifier] = new Parent {
                                xml = decElement, context = readerContext, parent = parent
                            };
                        }
                    }

                    if (!abstrct)
                    {
                        // Not an abstract dec instance, so create our instance
                        var decInstance = (Dec)typeHandle.CreateInstanceSafe("dec", () => $"{stringName}:{decElement.LineNumber()}");

                        // Error reporting happens within CreateInstanceSafe; if we get null out, we just need to clean up elegantly
                        if (decInstance != null)
                        {
                            decInstance.DecName = decName;

                            Database.Register(decInstance);

                            if (parent == null)
                            {
                                // Non-parent objects are simple; we just handle them here in order to avoid unnecessary GC churn
                                finishWork.Add(() => Serialization.ParseElement(decElement, typeHandle, decInstance, readerContext, new Recorder.Context(), isRootDec: true));
                            }
                            else
                            {
                                // Add an inheritance resolution job; we'll take care of this soon
                                inheritanceJobs.Add(new InheritanceJob {
                                    target = decInstance, xml = decElement, context = readerContext, parent = parent
                                });
                            }
                        }
                    }
                }
            }
        }
Ejemplo n.º 2
0
        /// <summary>
        /// Finish all parsing.
        /// </summary>
        public void Finish()
        {
            if (s_Status != Status.Accumulating)
            {
                Dbg.Err($"Finishing while the world is in {s_Status} state; should be {Status.Accumulating} state");
            }
            s_Status = Status.Processing;

            // We've successfully hit the Finish call, so let's stop spitting out empty warnings.
            Database.SuppressEmptyWarning();

            // Resolve all our inheritance jobs
            foreach (var work in inheritanceJobs)
            {
                // These are the actions we need to perform; we actually have to resolve these backwards (it makes their construction a little easier)
                // The final parse is listed first, then all the children up to the final point
                var actions = new List <Action>();

                actions.Add(() => Serialization.ParseElement(work.xml, work.target.GetType(), work.target, work.context, new Recorder.Context(), isRootDec: true));

                string        currentDecName = work.target.DecName;
                XElement      currentXml     = work.xml;
                ReaderContext currentContext = work.context;

                string parentDecName = work.parent;
                while (parentDecName != null)
                {
                    var parentData = potentialParents.TryGetValue(Tuple.Create(work.target.GetType().GetDecRootType(), parentDecName));

                    // This is a struct for the sake of performance, so child itself won't be null
                    if (parentData.xml == null)
                    {
                        Dbg.Err($"{currentContext.sourceName}:{currentXml.LineNumber()}: Dec {currentDecName} is attempting to use parent {parentDecName}, but no such dec exists");

                        // Not much more we can do here.
                        break;
                    }

                    actions.Add(() => Serialization.ParseElement(parentData.xml, work.target.GetType(), work.target, parentData.context, new Recorder.Context(), isRootDec: true));

                    currentDecName = parentDecName;
                    currentXml     = parentData.xml;
                    currentContext = parentData.context;

                    parentDecName = parentData.parent;
                }

                finishWork.Add(() =>
                {
                    for (int i = actions.Count - 1; i >= 0; --i)
                    {
                        actions[i]();
                    }
                });
            }

            foreach (var work in finishWork)
            {
                work();
            }

            if (s_Status != Status.Processing)
            {
                Dbg.Err($"Distributing while the world is in {s_Status} state; should be {Status.Processing} state");
            }
            s_Status = Status.Distributing;

            foreach (var stat in staticReferences)
            {
                if (!StaticReferencesAttribute.StaticReferencesFilled.Contains(stat))
                {
                    s_StaticReferenceHandler = () =>
                    {
                        s_StaticReferenceHandler = null;
                        StaticReferencesAttribute.StaticReferencesFilled.Add(stat);
                    };
                }

                bool touched = false;
                foreach (var field in stat.GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static))
                {
                    var dec = Database.Get(field.FieldType, field.Name);
                    if (dec == null)
                    {
                        Dbg.Err($"Failed to find {field.FieldType} declaration named {field.Name}");
                        field.SetValue(null, null); // this is unnecessary, but it does kick the static constructor just in case we wouldn't do it otherwise
                    }
                    else if (!field.FieldType.IsAssignableFrom(dec.GetType()))
                    {
                        Dbg.Err($"Static reference {field.FieldType} {stat}.{field.Name} is not compatible with {dec.GetType()} {dec}");
                        field.SetValue(null, null); // this is unnecessary, but it does kick the static constructor just in case we wouldn't do it otherwise
                    }
                    else
                    {
                        field.SetValue(null, dec);
                    }

                    touched = true;
                }

                if (s_StaticReferenceHandler != null)
                {
                    if (touched)
                    {
                        // Otherwise we shouldn't even expect this to have been registered, but at least there's literally no fields in it so it doesn't matter
                        Dbg.Err($"Failed to properly register {stat}; you may be missing a call to Dec.StaticReferencesAttribute.Initialized() in its static constructor, or the class may already have been initialized elsewhere (this should have thrown an error)");
                    }

                    s_StaticReferenceHandler = null;
                }
            }

            if (s_Status != Status.Distributing)
            {
                Dbg.Err($"Finalizing while the world is in {s_Status} state; should be {Status.Distributing} state");
            }
            s_Status = Status.Finalizing;

            foreach (var dec in Database.List)
            {
                try
                {
                    dec.ConfigErrors(err => Dbg.Err($"{dec.GetType()} {dec}: {err}"));
                }
                catch (Exception e)
                {
                    Dbg.Ex(e);
                }
            }

            foreach (var dec in Database.List)
            {
                try
                {
                    dec.PostLoad(err => Dbg.Err($"{dec.GetType()} {dec}: {err}"));
                }
                catch (Exception e)
                {
                    Dbg.Ex(e);
                }
            }

            if (s_Status != Status.Finalizing)
            {
                Dbg.Err($"Completing while the world is in {s_Status} state; should be {Status.Finalizing} state");
            }
            s_Status = Status.Finished;
        }
Ejemplo n.º 3
0
        /// <summary>
        /// Parses the output of Write, generating an object and all its related serialized data.
        /// </summary>
        public static T Read <T>(string input, string stringName = "input")
        {
            XDocument doc;

            try
            {
                doc = XDocument.Parse(input, LoadOptions.SetLineInfo);
            }
            catch (System.Xml.XmlException e)
            {
                Dbg.Ex(e);
                return(default(T));
            }

            if (doc.Elements().Count() > 1)
            {
                // This isn't testable, unfortunately; XDocument doesn't even support multiple root elements.
                Dbg.Err($"{stringName}: Found {doc.Elements().Count()} root elements instead of the expected 1");
            }

            var record = doc.Elements().First();

            if (record.Name.LocalName != "Record")
            {
                Dbg.Wrn($"{stringName}:{record.LineNumber()}: Found root element with name \"{record.Name.LocalName}\" when it should be \"Record\"");
            }

            var recordFormatVersion = record.ElementNamed("recordFormatVersion");

            if (recordFormatVersion == null)
            {
                Dbg.Err($"{stringName}:{record.LineNumber()}: Missing record format version, assuming the data is up-to-date");
            }
            else if (recordFormatVersion.GetText() != "1")
            {
                Dbg.Err($"{stringName}:{recordFormatVersion.LineNumber()}: Unknown record format version {recordFormatVersion.GetText()}, expected 1 or earlier");

                // I would rather not guess about this
                return(default(T));
            }

            var refs = record.ElementNamed("refs");

            var readerContext = new ReaderContext(stringName, true);

            if (refs != null)
            {
                // First, we need to make the instances for all the references, so they can be crosslinked appropriately
                foreach (var reference in refs.Elements())
                {
                    if (reference.Name.LocalName != "Ref")
                    {
                        Dbg.Wrn($"{stringName}:{reference.LineNumber()}: Reference element should be named 'Ref'");
                    }

                    var id = reference.Attribute("id")?.Value;
                    if (id == null)
                    {
                        Dbg.Err($"{stringName}:{reference.LineNumber()}: Missing reference ID");
                        continue;
                    }

                    var className = reference.Attribute("class")?.Value;
                    if (className == null)
                    {
                        Dbg.Err($"{stringName}:{reference.LineNumber()}: Missing reference class name");
                        continue;
                    }

                    var possibleType = (Type)Serialization.ParseString(className, typeof(Type), null, stringName, reference.LineNumber());
                    if (possibleType.IsValueType)
                    {
                        Dbg.Err($"{stringName}:{reference.LineNumber()}: Reference assigned type {possibleType}, which is a value type");
                        continue;
                    }

                    // Create a stub so other things can reference it later
                    readerContext.refs[id] = possibleType.CreateInstanceSafe("object", () => $"{stringName}:{reference.LineNumber()}");
                    // Might be null; that's okay, CreateInstanceSafe has done the error reporting
                }

                // Now that all the refs exist, we can run through them again and actually parse them
                foreach (var reference in refs.Elements())
                {
                    var id = reference.Attribute("id")?.Value;
                    if (id == null)
                    {
                        // Just skip it, we don't have anything useful we can do here
                        continue;
                    }

                    // The serialization routines don't know how to deal with this, so we'll remove it now
                    reference.Attribute("id").Remove();

                    var refInstance = readerContext.refs.TryGetValue(id);
                    if (refInstance == null)
                    {
                        // We failed to parse this for some reason, so just skip it now
                        continue;
                    }

                    // Do our actual parsing
                    var refInstanceOutput = Serialization.ParseElement(reference, refInstance.GetType(), refInstance, readerContext, hasReferenceId: true);

                    if (refInstance != refInstanceOutput)
                    {
                        Dbg.Err($"{stringName}:{reference.LineNumber()}: Something really bizarre has happened and we got the wrong object back. Things are probably irrevocably broken. Please report this as a bug in Dec.");
                        continue;
                    }
                }
            }

            var data = record.ElementNamed("data");

            if (data == null)
            {
                Dbg.Err($"{stringName}:{record.LineNumber()}: No data element provided. This is not very recoverable.");

                return(default(T));
            }

            // And now, we can finally parse our actual root element!
            // (which accounts for a tiny percentage of things that need to be parsed)
            return((T)Serialization.ParseElement(data, typeof(T), null, readerContext));
        }