/// <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)); }
/// <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, 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, 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} 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; }
/// <summary> /// Runs the Null writer, which is mostly useful for performance testing. /// </summary> public static void WriteNull <T>(T target) { var writerContext = new WriterNull(false); Serialization.ComposeElement(WriterNodeNull.Start(writerContext), target, target != null ? target.GetType() : typeof(T)); }
/// <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, 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 }); } } } } } }