diff --git a/Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs b/Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs index 30f39f6d..23c5a1b1 100644 --- a/Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs +++ b/Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs @@ -24,6 +24,8 @@ namespace Bonsai.Expressions public sealed class IncludeWorkflowBuilder : VariableArgumentExpressionBuilder, IGroupWorkflowBuilder, INamedElement, IRequireBuildContext { const char AssemblySeparator = ':'; + internal const string BuildUriPrefix = "::build:"; + const string PlaceholderSerializerPropertyName = "__Property__"; static readonly XElement[] EmptyProperties = new XElement[0]; static readonly XmlSerializerNamespaces DefaultSerializerNamespaces = GetXmlSerializerNamespaces(); @@ -103,7 +105,7 @@ public override Range ArgumentRange { if (workflow == null) { - try { EnsureWorkflow(); } + try { EnsureWorkflow(null); } catch (Exception) { } } return base.ArgumentRange; @@ -118,7 +120,7 @@ IBuildContext IRequireBuildContext.BuildContext buildContext = value; if (buildContext != null) { - EnsureWorkflow(); + EnsureWorkflow(buildContext); InternalXmlProperties = null; } } @@ -157,7 +159,7 @@ public XElement[] PropertiesXml if (InternalXmlProperties != null) return InternalXmlProperties; else if (workflow != null) { - return GetXmlProperties(); + return GetXmlProperties(workflow); } else return EmptyProperties; } @@ -173,18 +175,24 @@ static XmlSerializerNamespaces GetXmlSerializerNamespaces() return serializerNamespaces; } - XElement[] GetXmlProperties() + static XElement[] GetXmlProperties(ExpressionBuilderGraph workflow) { - var properties = TypeDescriptor.GetProperties(this); + if (workflow is null) + throw new ArgumentNullException(nameof(workflow)); + + var properties = TypeDescriptor.GetProperties(workflow); return GetXmlSerializableProperties(properties) - .Select(SerializeProperty) + .Select(property => SerializeProperty(workflow, property)) .Where(element => element != null) .ToArray(); } - void SetXmlProperties(XElement[] xmlProperties) + static void SetXmlProperties(ExpressionBuilderGraph workflow, XElement[] xmlProperties) { - var properties = TypeDescriptor.GetProperties(this); + if (workflow is null) + throw new ArgumentNullException(nameof(workflow)); + + var properties = TypeDescriptor.GetProperties(workflow); var serializableProperties = GetXmlSerializableProperties(properties).ToDictionary(property => property.Name); for (int i = 0; i < xmlProperties.Length; i++) { @@ -194,14 +202,14 @@ void SetXmlProperties(XElement[] xmlProperties) { var value = xmlProperties[i].Value; property = (ExternalizedPropertyDescriptor)properties[property.Name]; - property.SetValue(this, property.Converter.ConvertFromInvariantString(value)); + property.SetValue(workflow, property.Converter.ConvertFromInvariantString(value)); } - else DeserializeProperty(xmlProperties[i], property); + else DeserializeProperty(workflow, xmlProperties[i], property); } } } - IEnumerable GetXmlSerializableProperties(PropertyDescriptorCollection properties) + static IEnumerable GetXmlSerializableProperties(PropertyDescriptorCollection properties) { return from property in properties.Cast() let externalizedProperty = EnsureXmlSerializable(property as ExternalizedPropertyDescriptor) @@ -209,7 +217,7 @@ IEnumerable GetXmlSerializableProperties(Propert select externalizedProperty; } - ExternalizedPropertyDescriptor EnsureXmlSerializable(ExternalizedPropertyDescriptor descriptor) + static ExternalizedPropertyDescriptor EnsureXmlSerializable(ExternalizedPropertyDescriptor descriptor) { if (descriptor == null) return null; var xmlIgnore = descriptor.Attributes[typeof(XmlIgnoreAttribute)]; @@ -232,21 +240,24 @@ ExternalizedPropertyDescriptor EnsureXmlSerializable(ExternalizedPropertyDescrip return descriptor; } - void DeserializeProperty(XElement element, PropertyDescriptor property) + static void DeserializeProperty(ExpressionBuilderGraph workflow, XElement element, PropertyDescriptor property) { if (property.PropertyType == typeof(XElement)) { - property.SetValue(this, element); + property.SetValue(workflow, element); return; } - var serializer = PropertySerializer.GetXmlSerializer(property.Name, property.PropertyType); - using (var reader = element.CreateReader()) + var previousName = element.Name; + element.Name = XName.Get(PlaceholderSerializerPropertyName, element.Name.NamespaceName); + try { + var serializer = PropertySerializer.GetXmlSerializer(property.PropertyType); + using var reader = element.CreateReader(); var value = serializer.Deserialize(reader); if (property.IsReadOnly) { - var collection = (IList)property.GetValue(this); + var collection = (IList)property.GetValue(workflow); if (collection == null) { throw new InvalidOperationException("Collection reference not set to an instance of an object."); @@ -261,22 +272,29 @@ void DeserializeProperty(XElement element, PropertyDescriptor property) } } } - else property.SetValue(this, value); + else property.SetValue(workflow, value); + } + finally + { + element.Name = previousName; } } - XElement SerializeProperty(ExternalizedPropertyDescriptor property) + static XElement SerializeProperty(ExpressionBuilderGraph workflow, ExternalizedPropertyDescriptor property) { - var value = property.GetValue(this, out bool allEqual); + var value = property.GetValue(workflow, out bool allEqual); if (!allEqual) return null; var document = new XDocument(); - var serializer = PropertySerializer.GetXmlSerializer(property.Name, property.PropertyType); + var serializer = PropertySerializer.GetXmlSerializer(property.PropertyType); using (var writer = document.CreateWriter()) { serializer.Serialize(writer, value, DefaultSerializerNamespaces); } - return document.Root; + + var element = document.Root; + element.Name = XName.Get(property.Name, element.Name.NamespaceName); + return element; } static string GetWorkflowPath(string path) @@ -289,7 +307,7 @@ static string GetWorkflowPath(string path) return path; } - static bool IsEmbeddedResourcePath(string path) + internal static bool IsEmbeddedResourcePath(string path) { var separatorIndex = path.IndexOf(AssemblySeparator); return separatorIndex >= 0 && !SystemPath.IsPathRooted(path); @@ -307,7 +325,7 @@ static string GetDisplayName(string path) return name; } - static Stream GetWorkflowStream(string path, bool embeddedResource) + internal static Stream GetWorkflowStream(string path, bool embeddedResource) { if (embeddedResource) { @@ -333,16 +351,15 @@ static Stream GetWorkflowStream(string path, bool embeddedResource) } else { - if (!File.Exists(path)) + try { return File.OpenRead(path); } + catch (FileNotFoundException ex) { - throw new InvalidOperationException("The specified workflow could not be found."); + throw new InvalidOperationException("The specified workflow could not be found.", ex); } - - return File.OpenRead(path); } } - void EnsureWorkflow() + void EnsureWorkflow(IBuildContext buildContext) { var context = buildContext; while (context != null) @@ -366,32 +383,30 @@ void EnsureWorkflow() else { var embeddedResource = IsEmbeddedResourcePath(path); + var baseUri = buildContext != null ? $"{BuildUriPrefix}{path}" : path; var lastWriteTime = embeddedResource ? DateTime.MaxValue : File.GetLastWriteTime(path); if (workflow == null || lastWriteTime > writeTime) { - var properties = workflow != null ? GetXmlProperties() : InternalXmlProperties; + WorkflowBuilder builder; + var properties = workflow != null ? GetXmlProperties(workflow) : InternalXmlProperties; using (var stream = GetWorkflowStream(path, embeddedResource)) - using (var reader = XmlReader.Create(stream)) + using (var reader = XmlReader.Create(stream, null, baseUri)) { reader.MoveToContent(); var serializer = new XmlSerializer(typeof(WorkflowBuilder), reader.NamespaceURI); - var builder = (WorkflowBuilder)serializer.Deserialize(reader); - description = builder.Description; - workflow = builder.Workflow; - writeTime = lastWriteTime; - } - - var parameterCount = workflow.GetNestedParameters().Count(); - SetArgumentRange(0, parameterCount); - if (inspectWorkflow) - { - workflow = workflow.ToInspectableGraph(); + builder = (WorkflowBuilder)serializer.Deserialize(reader); } if (properties != null) { - SetXmlProperties(properties); + SetXmlProperties(builder.Workflow, properties); } + + var parameterCount = builder.Workflow.GetNestedParameters().Count(); + workflow = inspectWorkflow ? builder.Workflow.ToInspectableGraph() : builder.Workflow; + description = builder.Description; + SetArgumentRange(0, parameterCount); + writeTime = lastWriteTime; } } } @@ -419,22 +434,24 @@ public override Expression Build(IEnumerable arguments) return workflow.BuildNested(arguments, includeContext); } + // We serialize all properties using the same placeholder root name to allow us to reuse + // a single serializer for each different property type. This means we need to rename + // the actual XElement name before or after serialization and deserialization. static class PropertySerializer { - static readonly Dictionary, XmlSerializer> serializerCache = new Dictionary, XmlSerializer>(); - static readonly object cacheLock = new object(); + static readonly Dictionary serializerCache = new(); + static readonly object cacheLock = new(); - internal static XmlSerializer GetXmlSerializer(string name, Type type) + internal static XmlSerializer GetXmlSerializer(Type type) { XmlSerializer serializer; - var serializerKey = Tuple.Create(name, type); lock (cacheLock) { - if (!serializerCache.TryGetValue(serializerKey, out serializer)) + if (!serializerCache.TryGetValue(type, out serializer)) { - var xmlRoot = new XmlRootAttribute(name) { Namespace = Constants.XmlNamespace }; + var xmlRoot = new XmlRootAttribute(PlaceholderSerializerPropertyName) { Namespace = Constants.XmlNamespace }; serializer = new XmlSerializer(type, xmlRoot); - serializerCache.Add(serializerKey, serializer); + serializerCache.Add(type, serializer); } } @@ -541,15 +558,17 @@ public override bool CanResetValue(object component) public override object GetValue(object component) { - if (!(component is IncludeWorkflowBuilder includeWorkflow)) + var workflow = component switch { - throw new ArgumentException("Incompatible component type in workflow property assignment.", nameof(component)); - } + IncludeWorkflowBuilder includeWorkflow => includeWorkflow.Workflow, + ExpressionBuilderGraph workflowComponent => workflowComponent, + _ => throw new ArgumentException("Incompatible component type in workflow property assignment.", nameof(component)) + }; - var serializableProperty = includeWorkflow.EnsureXmlSerializable(property); + var serializableProperty = EnsureXmlSerializable(property); if (serializableProperty != null) { - return includeWorkflow.SerializeProperty(serializableProperty); + return SerializeProperty(workflow, serializableProperty); } return null; @@ -562,20 +581,22 @@ public override void ResetValue(object component) public override void SetValue(object component, object value) { - if (!(value is XElement element)) + if (value is not XElement element) { throw new ArgumentException("Incompatible types found in workflow property assignment.", nameof(value)); } - if (!(component is IncludeWorkflowBuilder includeWorkflow)) + var workflow = component switch { - throw new ArgumentException("Incompatible component type in workflow property assignment.", nameof(component)); - } + IncludeWorkflowBuilder includeWorkflow => includeWorkflow.Workflow, + ExpressionBuilderGraph workflowComponent => workflowComponent, + _ => throw new ArgumentException("Incompatible component type in workflow property assignment.", nameof(component)) + }; - var serializableProperty = includeWorkflow.EnsureXmlSerializable(property); + var serializableProperty = EnsureXmlSerializable(property); if (serializableProperty != null) { - includeWorkflow.DeserializeProperty(element, serializableProperty); + DeserializeProperty(workflow, element, serializableProperty); } } diff --git a/Bonsai.Core/WorkflowBuilder.cs b/Bonsai.Core/WorkflowBuilder.cs index 232dc164..58a43759 100644 --- a/Bonsai.Core/WorkflowBuilder.cs +++ b/Bonsai.Core/WorkflowBuilder.cs @@ -100,6 +100,12 @@ public static WorkflowMetadata ReadMetadata(string inputUri) /// A instance containing the retrieved metadata. /// public static WorkflowMetadata ReadMetadata(XmlReader reader) + { + var visitedWorkflows = new HashSet(); + return ReadMetadata(reader, visitedWorkflows); + } + + static WorkflowMetadata ReadMetadata(XmlReader reader, HashSet visitedWorkflows) { var metadata = new WorkflowMetadata(); var serializerNamespaces = new SerializerNamespaces(); @@ -128,7 +134,7 @@ public static WorkflowMetadata ReadMetadata(XmlReader reader) { workflowMarkup = ConvertDescriptorMarkup(reader.ReadOuterXml()); } - else workflowMarkup = ReadXmlExtensions(reader, types, serializerNamespaces); + else workflowMarkup = ReadXmlExtensions(reader, types, visitedWorkflows, serializerNamespaces); } if (reader.ReadToNextSibling(ExtensionTypeNodeName)) @@ -824,7 +830,14 @@ static Type ResolveXmlExtension(XmlReader reader, string value, string typeArgum return null; } - static void WriteXmlAttributes(XmlReader reader, XmlWriter writer, bool lookupTypes, HashSet types, SerializerNamespaces namespaces, ref bool includeWorkflow) + static void WriteXmlAttributes( + XmlReader reader, + XmlWriter writer, + bool lookupTypes, + HashSet types, + HashSet visitedWorkflows, + SerializerNamespaces namespaces, + ref bool includeWorkflow) { do { @@ -888,6 +901,29 @@ static void WriteXmlAttributes(XmlReader reader, XmlWriter writer, bool lookupTy value = XmlConvert.EncodeName(typeName); } } + else if (includeWorkflow && + reader.GetAttribute(nameof(IncludeWorkflowBuilder.Path)) is string path && + !reader.BaseURI.StartsWith(IncludeWorkflowBuilder.BuildUriPrefix) && + visitedWorkflows.Add(path)) + { + // we don't want to fail in most cases while reading nested metadata, as this + // is an optional performance optimization and we would lose the visual context + // as to where exactly in the workflow the failure is happening + try + { + var embeddedResource = IncludeWorkflowBuilder.IsEmbeddedResourcePath(path); + using var workflowStream = IncludeWorkflowBuilder.GetWorkflowStream(path, embeddedResource); + using var workflowReader = XmlReader.Create(workflowStream, null, path); + workflowReader.MoveToContent(); + var nestedMetadata = ReadMetadata(workflowReader, visitedWorkflows); + types.UnionWith(nestedMetadata.Types); + } + catch (IOException) { } + catch (XmlException) { } + catch (BadImageFormatException) { } + catch (InvalidOperationException) { } + catch (UnauthorizedAccessException) { } + } } writer.WriteString(value); @@ -912,7 +948,7 @@ static void WriteXmlAttributes(XmlReader reader, XmlWriter writer, bool lookupTy while (reader.MoveToNextAttribute()); } - static string ReadXmlExtensions(XmlReader reader, HashSet types, SerializerNamespaces namespaces) + static string ReadXmlExtensions(XmlReader reader, HashSet types, HashSet visitedWorkflows, SerializerNamespaces namespaces) { const int ChunkBufferSize = 1024; char[] chunkBuffer = null; @@ -944,7 +980,7 @@ static string ReadXmlExtensions(XmlReader reader, HashSet types, Serialize { var includeWorkflow = includeDepth >= 0; var lookupTypes = elementNamespace == Constants.XmlNamespace; - WriteXmlAttributes(reader, writer, lookupTypes, types, serializerNamespaces, ref includeWorkflow); + WriteXmlAttributes(reader, writer, lookupTypes, types, visitedWorkflows, serializerNamespaces, ref includeWorkflow); reader.MoveToElement(); if (lookupTypes && includeDepth < 0 && includeWorkflow) {