diff --git a/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/AttributesTokenizer.cs b/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/AttributesTokenizer.cs new file mode 100644 index 0000000..0f3b3fe --- /dev/null +++ b/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/AttributesTokenizer.cs @@ -0,0 +1,277 @@ +using System; +using System.Collections; +using System.Text; + +namespace LogicBuilder.Workflow.ComponentModel.Serialization +{ + internal class AttributesTokenizer : IAttributesTokenizer + { + private ArrayList list = null; + private int length = 0; + private bool inQuotes = false; + private bool gotEscape = false; + private bool nonWhitespaceFound = false; + private char quoteChar = '\''; + private int leftCurlies = 0; + private bool collectionIndexer = false; + private StringBuilder stringBuilder = null; + private int i = 0; + private enum LoopControl + { + Continue, + Break, + Error + } + + // This function splits the argument string into an array of tokens. + // For example: ID=Workflow1, Path=error1} would become an array that contains the following elements + // {ID} {=} {Workflwo1} {,} {Path} {=} {error1} + // Note that the input string should start with the first argument and end with '}'. + public ArrayList TokenizeAttributes(string args) + { + length = args.Length; + + // Loop through the args, creating a list of arguments and known delimiters. + // This loop does limited syntax checking, and serves to tokenize the argument + // string into chunks that are validated in greater detail in the next phase. + for (; i < length; i++) + { + // Escape character is always in effect for everything inside + // a MarkupExtension. We have to remember that the next character is + // escaped, and is not treated as a quote or delimiter. + if (CanSetGotEscapeToTrue(args)) + { + gotEscape = true; + continue; + } + + if (CanSetNonWhitespaceFoundToTrue(args)) + nonWhitespaceFound = true; + + // Process all characters that are not whitespace or are between quotes + if (NotCharacterForProcessing()) + continue; + + // We have a non-whitespace character, so ensure we have + // a string builder to accumulate characters and a list to collect + // attributes and delimiters. These are lazily + // created so that simple cases that have no parameters do not + // create any extra objects. + if (stringBuilder == null) + { + stringBuilder = new StringBuilder(length); + list = new ArrayList(1); + } + + var loopControl = ProcessChar(args); + if (loopControl == LoopControl.Break) + break; + else if (loopControl == LoopControl.Error) + return []; + } + + ValidateTheStringBuilder(args); + + return list; + } + + private LoopControl ProcessChar(string args) + { + // If the character is escaped, then it is part of the attribute + // being collected, regardless of its value and is not treated as + // a delimiter or special character. Write back the escape + // character since downstream processing will need it to determine + // whether the value is a MarkupExtension or not, and to prevent + // multiple escapes from being lost by recursive processing. + if (gotEscape) + { + stringBuilder.Append('\\'); + stringBuilder.Append(args[i]); + gotEscape = false; + return LoopControl.Continue; + } + + // If this characters is not escaped, then look for quotes and + // delimiters. + if (InsideQuotesOrCurlies()) + { + ProcessInQuotesAndCurlyBraces(args); + return LoopControl.Continue; + } + + if (IsQuoteCharacter(args)) + { + if (!ProcessQuoteChars(args)) + return LoopControl.Error; + } + else if (IsDelimiter(args)) + { + if (!ProcessDelimiters(args)) + return LoopControl.Error; + } + else if (args[i] == '}') + { + if (!ProcessRightCurly()) + return LoopControl.Error; + + return LoopControl.Break; + } + else + { + ProcessPlainCharacter(args); + } + + return LoopControl.Continue; + } + + private bool CanSetNonWhitespaceFoundToTrue(string args) + { + return !nonWhitespaceFound && !char.IsWhiteSpace(args[i]); + } + + private bool CanSetGotEscapeToTrue(string args) + { + return !gotEscape && args[i] == '\\'; + } + + private bool IsDelimiter(string args) + { + return args[i] == ',' || args[i] == '='; + } + + private bool IsQuoteCharacter(string args) + { + return (args[i] == '"' || args[i] == '\''); + } + + private bool InsideQuotesOrCurlies() + { + return inQuotes || leftCurlies > 0; + } + + private bool NotCharacterForProcessing() + { + return !inQuotes && leftCurlies <= 0 && !nonWhitespaceFound; + } + + private void ValidateTheStringBuilder(string args) + { + // If we've accumulated content but haven't hit a terminating '}' then the + // format is bad, so complain. + if (stringBuilder != null && stringBuilder.Length > 0) + throw new InvalidOperationException(SR.GetString(SR.Error_MarkupExtensionMissingTerminatingCharacter)); + else if (i < length) + { + // If there is non-whitespace text left that we haven't processes yet, + // then there is junk after the closing '}', so complain + for (++i; i < length; i++) + { + if (!Char.IsWhiteSpace(args[i])) + throw new InvalidOperationException(SR.GetString(SR.Error_ExtraCharacterFoundAtEnd)); + } + } + } + + private void ProcessPlainCharacter(string args) + { + if (args[i] == '{') + { + leftCurlies++; + } + // Must just be a plain old character, so add it to the stringbuilder + stringBuilder.Append(args[i]); + } + + private bool ProcessRightCurly() + { + // If we hit the outside right curly brace, then end processing. If + // there is a delimiter on the top of the stack and we haven't + // hit another non-whitespace character, then its an error + if (stringBuilder != null) + { + if (stringBuilder.Length > 0) + { + list.Add(stringBuilder.ToString().Trim()); + stringBuilder.Length = 0; + } + else if (list.Count > 0 && (list[list.Count - 1] is char)) + return false; + } + + return true; + } + + private bool ProcessDelimiters(string args) + { + // If there is something in the stringbuilder, then store it + if (stringBuilder != null && stringBuilder.Length > 0) + { + list.Add(stringBuilder.ToString().Trim()); + stringBuilder.Length = 0; + } + else if (list.Count == 0 || list[list.Count - 1] is Char) + { + // Can't have two delimiters in a row, so check what is on + // the list and complain if the last item is a character, or if + // a delimiter is the first item. + return false; + } + + // Append known delimiters. + list.Add(args[i]); + nonWhitespaceFound = false; + return true; + } + + private bool ProcessQuoteChars(string args) + { + // If we're not inside quotes, then a start quote can only + // occur as the first non-whitespace character in a name or value. + if (collectionIndexer && i < args.Length - 1 && args[i + 1] == ']') + { + collectionIndexer = false; + stringBuilder.Append(args[i]); + } + else if (i > 0 && args[i - 1] == '[') + { + collectionIndexer = true; + stringBuilder.Append(args[i]); + } + else + { + if (stringBuilder.Length != 0) + return false; + + inQuotes = true; + quoteChar = args[i]; + } + + return true; + } + + private void ProcessInQuotesAndCurlyBraces(string args) + { + if (inQuotes && args[i] == quoteChar) + { + // If we're inside quotes, then only an end quote that is not + // escaped is special, and will act as a delimiter. + inQuotes = false; + list.Add(stringBuilder.ToString()); + stringBuilder.Length = 0; + nonWhitespaceFound = false; + } + else + { + if (leftCurlies > 0 && args[i] == '}') + { + leftCurlies--; + } + else if (args[i] == '{') + { + leftCurlies++; + } + stringBuilder.Append(args[i]); + } + } + } +} diff --git a/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/IAttributesTokenizer.cs b/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/IAttributesTokenizer.cs new file mode 100644 index 0000000..fbcf53c --- /dev/null +++ b/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/IAttributesTokenizer.cs @@ -0,0 +1,9 @@ +using System.Collections; + +namespace LogicBuilder.Workflow.ComponentModel.Serialization +{ + internal interface IAttributesTokenizer + { + ArrayList TokenizeAttributes(string args); + } +} diff --git a/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/MarkupExtensionSerializer.cs b/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/MarkupExtensionSerializer.cs index 123278e..896a96c 100644 --- a/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/MarkupExtensionSerializer.cs +++ b/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/MarkupExtensionSerializer.cs @@ -31,10 +31,10 @@ protected internal sealed override bool CanSerializeToString(WorkflowMarkupSeria protected internal sealed override string SerializeToString(WorkflowMarkupSerializationManager serializationManager, object value) { if (serializationManager == null) - throw new ArgumentNullException("serializationManager"); - XmlWriter writer = serializationManager.WorkflowMarkupStack[typeof(XmlWriter)] as XmlWriter ?? throw new ArgumentNullException("writer"); + throw new ArgumentNullException(nameof(serializationManager)); + XmlWriter writer = serializationManager.WorkflowMarkupStack[typeof(XmlWriter)] as XmlWriter ?? throw new InvalidOperationException("Writer is null."); if (value == null) - throw new ArgumentNullException("value"); + throw new ArgumentNullException(nameof(value)); writer.WriteString(MarkupExtensionSerializer.CompactFormatStart); XmlQualifiedName qualifiedName = serializationManager.GetXmlQualifiedName(value.GetType(), out _); @@ -46,46 +46,7 @@ protected internal sealed override string SerializeToString(WorkflowMarkupSerial InstanceDescriptor instanceDescriptor = this.GetInstanceDescriptor(serializationManager, value); if (instanceDescriptor != null && instanceDescriptor.MemberInfo is ConstructorInfo ctorInfo) { - ParameterInfo[] parameters = ctorInfo.GetParameters(); - if (parameters != null && parameters.Length == instanceDescriptor.Arguments.Count) - { - int i = 0; - foreach (object argValue in instanceDescriptor.Arguments) - { - constructorArguments ??= []; - // - if (argValue == null) - continue; - constructorArguments.Add(parameters[i].Name, parameters[i++].Name); - if (index++ > 0) - writer.WriteString(MarkupExtensionSerializer.CompactFormatPropertySeperator); - else - writer.WriteString(MarkupExtensionSerializer.CompactFormatTypeSeperator); - if (argValue.GetType() == typeof(string)) - { - writer.WriteString(CreateEscapedValue(argValue as string)); - } - else if (argValue is System.Type) - { - Type argType = argValue as Type; - if (argType?.Assembly != null) - { - XmlQualifiedName typeQualifiedName = serializationManager.GetXmlQualifiedName(argType, out _); - writer.WriteQualifiedName(XmlConvert.EncodeName(typeQualifiedName.Name), typeQualifiedName.Namespace); - } - else - { - writer.WriteString(argType?.FullName ?? string.Empty); - } - } - else - { - string stringValue = base.SerializeToString(serializationManager, argValue); - if (stringValue != null) - writer.WriteString(stringValue); - } - } - } + constructorArguments = GetConstructorArguments(serializationManager, writer, ref index, instanceDescriptor, ctorInfo); } List properties = @@ -94,13 +55,11 @@ .. GetProperties(serializationManager, value), .. serializationManager.GetExtendedProperties(value), ]; - foreach + foreach ( PropertyInfo serializableProperty in properties.Where ( - p => Helpers.GetSerializationVisibility(p) != DesignerSerializationVisibility.Hidden - && p.CanRead - && p.GetValue(value, null) != null + IsValidPropertyForSerialization(value) ) ) { @@ -119,53 +78,129 @@ PropertyInfo serializableProperty in properties.Where } //Get the property serializer so that we can convert the bind object to string - serializationManager.Context.Push(serializableProperty); - try + if (!ExecutePropetySerialization(serializationManager, value, writer, ref index, serializableProperty, propSerializer)) + continue; + } + + writer.WriteString(MarkupExtensionSerializer.CompactFormatEnd); + return string.Empty; + + } + + private static Func IsValidPropertyForSerialization(object value) + { + return p => Helpers.GetSerializationVisibility(p) != DesignerSerializationVisibility.Hidden + && p.CanRead + && p.GetValue(value, null) != null; + } + + private static bool ExecutePropetySerialization(WorkflowMarkupSerializationManager serializationManager, object value, XmlWriter writer, ref int index, PropertyInfo serializableProperty, WorkflowMarkupSerializer propSerializer) + { + //Get the property serializer so that we can convert the bind object to string + serializationManager.Context.Push(serializableProperty); + try + { + index = SerializeProperty(serializationManager, value, writer, index, serializableProperty, propSerializer); + } + catch (Exception ex) when (!ExceptionUtility.IsCriticalException(ex)) + { + serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerNoSerializeLogic, [serializableProperty.Name, value.GetType().FullName]), ex)); + return false; + } + finally + { + Debug.Assert((PropertyInfo)serializationManager.Context.Current == serializableProperty, "Serializer did not remove an object it pushed into stack."); + serializationManager.Context.Pop(); + } + + return true; + } + + private static int SerializeProperty(WorkflowMarkupSerializationManager serializationManager, object value, XmlWriter writer, int index, PropertyInfo serializableProperty, WorkflowMarkupSerializer propSerializer) + { + object propValue = serializableProperty.GetValue(value, null); + if (!propSerializer.ShouldSerializeValue(serializationManager, propValue)) + return index; + + //We do not allow nested bind syntax + if (propSerializer.CanSerializeToString(serializationManager, propValue)) + { + if (index++ > 0) + writer.WriteString(MarkupExtensionSerializer.CompactFormatPropertySeperator); + else + writer.WriteString(MarkupExtensionSerializer.CompactFormatTypeSeperator); + writer.WriteString(serializableProperty.Name); + writer.WriteString(MarkupExtensionSerializer.CompactFormatNameValueSeperator); + + if (propValue is string propValueString) { - object propValue = serializableProperty.GetValue(value, null); - if (propSerializer.ShouldSerializeValue(serializationManager, propValue)) - { - //We do not allow nested bind syntax - if (propSerializer.CanSerializeToString(serializationManager, propValue)) - { - if (index++ > 0) - writer.WriteString(MarkupExtensionSerializer.CompactFormatPropertySeperator); - else - writer.WriteString(MarkupExtensionSerializer.CompactFormatTypeSeperator); - writer.WriteString(serializableProperty.Name); - writer.WriteString(MarkupExtensionSerializer.CompactFormatNameValueSeperator); - - if (propValue.GetType() == typeof(string)) - { - writer.WriteString(CreateEscapedValue(propValue as string)); - } - else - { - string stringValue = propSerializer.SerializeToString(serializationManager, propValue); - if (stringValue != null) - writer.WriteString(stringValue); - } - } - else - { - serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerNoSerializeLogic, [serializableProperty.Name, value.GetType().FullName]))); - } - } + writer.WriteString(CreateEscapedValue(propValueString)); } - catch (Exception ex) when (!ExceptionUtility.IsCriticalException(ex)) + else { - serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerNoSerializeLogic, [serializableProperty.Name, value.GetType().FullName]), ex)); + string stringValue = propSerializer.SerializeToString(serializationManager, propValue); + if (stringValue != null) + writer.WriteString(stringValue); + } + } + else + { + serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerNoSerializeLogic, [serializableProperty.Name, value.GetType().FullName]))); + } + + return index; + } + + private Dictionary GetConstructorArguments(WorkflowMarkupSerializationManager serializationManager, XmlWriter writer, ref int index, InstanceDescriptor instanceDescriptor, ConstructorInfo ctorInfo) + { + Dictionary constructorArguments = null; + ParameterInfo[] parameters = ctorInfo.GetParameters(); + if (parameters == null || parameters.Length != instanceDescriptor.Arguments.Count) + return constructorArguments; + + int i = 0; + foreach (object argValue in instanceDescriptor.Arguments) + { + constructorArguments ??= []; + if (argValue == null) continue; + constructorArguments.Add(parameters[i].Name, parameters[i++].Name); + if (index++ > 0) + writer.WriteString(MarkupExtensionSerializer.CompactFormatPropertySeperator); + else + writer.WriteString(MarkupExtensionSerializer.CompactFormatTypeSeperator); + + if (argValue is string argValueString) + { + writer.WriteString(CreateEscapedValue(argValueString)); + } + else if (argValue is System.Type) + { + WriteTypeString(serializationManager, writer, argValue); } - finally + else { - Debug.Assert((PropertyInfo)serializationManager.Context.Current == serializableProperty, "Serializer did not remove an object it pushed into stack."); - serializationManager.Context.Pop(); + string stringValue = base.SerializeToString(serializationManager, argValue); + if (stringValue != null) + writer.WriteString(stringValue); } } - writer.WriteString(MarkupExtensionSerializer.CompactFormatEnd); - return string.Empty; + return constructorArguments; + } + + private static void WriteTypeString(WorkflowMarkupSerializationManager serializationManager, XmlWriter writer, object argValue) + { + Type argType = argValue as Type; + if (argType?.Assembly != null) + { + XmlQualifiedName typeQualifiedName = serializationManager.GetXmlQualifiedName(argType, out _); + writer.WriteQualifiedName(XmlConvert.EncodeName(typeQualifiedName.Name), typeQualifiedName.Namespace); + } + else + { + writer.WriteString(argType?.FullName ?? string.Empty); + } } protected virtual InstanceDescriptor GetInstanceDescriptor(WorkflowMarkupSerializationManager serializationManager, object value) @@ -176,7 +211,7 @@ protected virtual InstanceDescriptor GetInstanceDescriptor(WorkflowMarkupSeriali } // more escaped characters can be consider here, hence a seperate fn instead of string.Replace - private string CreateEscapedValue(string value) + private static string CreateEscapedValue(string value) { if (value == null) throw new ArgumentNullException("value"); diff --git a/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/WorkflowMarkupSerializationManager.cs b/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/WorkflowMarkupSerializationManager.cs index 083dedd..76c7acb 100644 --- a/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/WorkflowMarkupSerializationManager.cs +++ b/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/WorkflowMarkupSerializationManager.cs @@ -15,8 +15,6 @@ #region Class WorkflowMarkupSerializationManager public class WorkflowMarkupSerializationManager : IDesignerSerializationManager { - private Assembly localAssembly = null; - private int writerDepth = 0; private readonly ContextStack workflowMarkupStack = new(); // Stack to keep a list of objects being serialized, to avoid stack overflow private readonly Stack serializationStack = new(); @@ -97,17 +95,7 @@ public void RemoveSerializationProvider(IDesignerSerializationProvider provider) this.serializationManager.RemoveSerializationProvider(provider); } - public Assembly LocalAssembly - { - get - { - return this.localAssembly; - } - set - { - this.localAssembly = value; - } - } + public Assembly LocalAssembly { get; set; } = null; #region Public Methods public virtual XmlQualifiedName GetXmlQualifiedName(Type type, out string prefix) @@ -116,7 +104,7 @@ public virtual XmlQualifiedName GetXmlQualifiedName(Type type, out string prefix throw new ArgumentNullException("type"); string typeNamespace = type.Namespace ?? String.Empty; - string assemblyName = (type.Assembly != null && type.Assembly != this.localAssembly) ? type.Assembly.FullName : String.Empty; + string assemblyName = (type.Assembly != null && type.Assembly != LocalAssembly) ? type.Assembly.FullName : String.Empty; int key = typeNamespace.GetHashCode() ^ assemblyName.GetHashCode(); if (!this.clrNamespaceBasedMappings.TryGetValue(key, out WorkflowMarkupSerializerMapping mappingForType)) @@ -165,48 +153,7 @@ public virtual Type GetType(XmlQualifiedName xmlQualifiedName) foreach (WorkflowMarkupSerializerMapping xmlnsMapping in xmlnsMappings) { - string assemblyName = xmlnsMapping.AssemblyName; - string clrNamespace = xmlnsMapping.ClrNamespace; - - // append dot net namespace name - string fullTypeName = xmlQualifiedName.Name; - if (clrNamespace.Length > 0) - fullTypeName = clrNamespace + "." + xmlQualifiedName.Name; - - // Work around for component model assembly - if (assemblyName.Equals(Assembly.GetExecutingAssembly().FullName, StringComparison.Ordinal)) - { - resolvedType = Assembly.GetExecutingAssembly().GetType(fullTypeName); - } - else if (assemblyName.Length == 0) - { - if (this.localAssembly != null) - resolvedType = this.localAssembly.GetType(fullTypeName); - } - else - { - string assemblyQualifiedName = fullTypeName; - if (assemblyName.Length > 0) - assemblyQualifiedName += (", " + assemblyName); - - // now grab the actual type - try - { - resolvedType = GetType(assemblyQualifiedName); - } - catch (Exception ex) when (!ExceptionUtility.IsCriticalException(ex)) - { - // Intentionally ignore non-critical exceptions when resolving the type. - // Fallback logic below will handle the case where resolvedType remains null. - } - - if (resolvedType == null) - { - resolvedType = GetType(fullTypeName); - if (resolvedType != null && !resolvedType.AssemblyQualifiedName.Equals(assemblyQualifiedName, StringComparison.Ordinal)) - resolvedType = null; - } - } + resolvedType = ResolveType(xmlQualifiedName, resolvedType, xmlnsMapping); //We found the type if (resolvedType != null) @@ -219,6 +166,53 @@ public virtual Type GetType(XmlQualifiedName xmlQualifiedName) return resolvedType; } + + private Type ResolveType(XmlQualifiedName xmlQualifiedName, Type resolvedType, WorkflowMarkupSerializerMapping xmlnsMapping) + { + string assemblyName = xmlnsMapping.AssemblyName; + string clrNamespace = xmlnsMapping.ClrNamespace; + + // append dot net namespace name + string fullTypeName = clrNamespace.Length > 0 + ? clrNamespace + "." + xmlQualifiedName.Name + : xmlQualifiedName.Name; + + // Work around for component model assembly + if (assemblyName.Equals(Assembly.GetExecutingAssembly().FullName, StringComparison.Ordinal)) + { + resolvedType = Assembly.GetExecutingAssembly().GetType(fullTypeName); + } + else if (assemblyName.Length == 0 && LocalAssembly != null) + { + resolvedType = LocalAssembly.GetType(fullTypeName); + } + else + { + string assemblyQualifiedName = fullTypeName; + if (assemblyName.Length > 0) + assemblyQualifiedName += (", " + assemblyName); + + // now grab the actual type + try + { + resolvedType = GetType(assemblyQualifiedName); + } + catch (Exception ex) when (!ExceptionUtility.IsCriticalException(ex)) + { + // Intentionally ignore non-critical exceptions when resolving the type. + // Fallback logic below will handle the case where resolvedType remains null. + } + + if (resolvedType == null) + { + resolvedType = GetType(fullTypeName); + if (resolvedType != null && !resolvedType.AssemblyQualifiedName.Equals(assemblyQualifiedName, StringComparison.Ordinal)) + resolvedType = null; + } + } + + return resolvedType; + } #endregion #region WorkflowMarkupSerializationManager Overrides @@ -292,17 +286,7 @@ public virtual Type GetType(string typeName) #endregion #region Helpers - internal int WriterDepth - { - get - { - return this.writerDepth; - } - set - { - this.writerDepth = value; - } - } + internal int WriterDepth { get; set; } = 0; internal ContextStack WorkflowMarkupStack { @@ -403,9 +387,17 @@ PropertyDescriptorCollection IDesignerSerializationManager.Properties get { return this.serializationManager.Properties; } } - event ResolveNameEventHandler IDesignerSerializationManager.ResolveName { add { } remove { } } + event ResolveNameEventHandler IDesignerSerializationManager.ResolveName + { + add { this.serializationManager.ResolveName += value; } + remove { this.serializationManager.ResolveName -= value; } + } - event EventHandler IDesignerSerializationManager.SerializationComplete { add { } remove { } } + event EventHandler IDesignerSerializationManager.SerializationComplete + { + add { this.serializationManager.SerializationComplete += value; } + remove { this.serializationManager.SerializationComplete -= value; } + } void IDesignerSerializationManager.SetName(object instance, string name) { diff --git a/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/WorkflowMarkupSerializer.cs b/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/WorkflowMarkupSerializer.cs index c7180da..e896cf4 100644 --- a/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/WorkflowMarkupSerializer.cs +++ b/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/WorkflowMarkupSerializer.cs @@ -1631,20 +1631,24 @@ internal object DeserializeFromCompactFormat(WorkflowMarkupSerializationManager ArrayList argTokens; try { - argTokens = TokenizeAttributes(arguments); + IAttributesTokenizer attributesTokenizer = new AttributesTokenizer(); + argTokens = attributesTokenizer.TokenizeAttributes(arguments); } catch (Exception error) when (!ExceptionUtility.IsCriticalException(error)) { serializationManager.ReportError(CreateSerializationError(SR.GetString(SR.Error_MarkupExtensionDeserializeFailed, attrValue, error.Message), reader)); return null; } - if (argTokens != null) + if (argTokens != null && argTokens.Count > 0) { // Process the positional arugments and find the correct constructor to call. ArrayList positionalArgs = []; bool firstEqual = true; for (int i = 0; i < argTokens.Count; i++) { + if (i > 0 && argTokens[i - 1] is char previousArgToken && previousArgToken == '=') + continue; + char token = (argTokens[i] is char argToken) ? argToken : '\0'; if (token == '=') { @@ -1652,7 +1656,6 @@ internal object DeserializeFromCompactFormat(WorkflowMarkupSerializationManager positionalArgs.RemoveAt(positionalArgs.Count - 1); firstEqual = false; namedArgs.Add(argTokens[i - 1] as string, argTokens[i + 1] as string); - i++; } if (token == ',') continue; @@ -1753,195 +1756,9 @@ internal object DeserializeFromCompactFormat(WorkflowMarkupSerializationManager return obj; } - // This function splits the argument string into an array of tokens. - // For example: ID=Workflow1, Path=error1} would become an array that contains the following elements - // {ID} {=} {Workflwo1} {,} {Path} {=} {error1} - // Note that the input string should start with the first argument and end with '}'. - private ArrayList TokenizeAttributes(string args) - { - ArrayList list = null; - int length = args.Length; - bool inQuotes = false; - bool gotEscape = false; - bool nonWhitespaceFound = false; - Char quoteChar = '\''; - int leftCurlies = 0; - bool collectionIndexer = false; - - StringBuilder stringBuilder = null; - int i = 0; - - // Loop through the args, creating a list of arguments and known delimiters. - // This loop does limited syntax checking, and serves to tokenize the argument - // string into chunks that are validated in greater detail in the next phase. - for (; i < length; i++) - { - // Escape character is always in effect for everything inside - // a MarkupExtension. We have to remember that the next character is - // escaped, and is not treated as a quote or delimiter. - if (!gotEscape && args[i] == '\\') - { - gotEscape = true; - continue; - } - - if (!nonWhitespaceFound && !Char.IsWhiteSpace(args[i])) - { - nonWhitespaceFound = true; - } - - // Process all characters that are not whitespace or are between quotes - if (inQuotes || leftCurlies > 0 || nonWhitespaceFound) - { - // We have a non-whitespace character, so ensure we have - // a string builder to accumulate characters and a list to collect - // attributes and delimiters. These are lazily - // created so that simple cases that have no parameters do not - // create any extra objects. - if (stringBuilder == null) - { - stringBuilder = new StringBuilder(length); - list = new ArrayList(1); - } - - // If the character is escaped, then it is part of the attribute - // being collected, regardless of its value and is not treated as - // a delimiter or special character. Write back the escape - // character since downstream processing will need it to determine - // whether the value is a MarkupExtension or not, and to prevent - // multiple escapes from being lost by recursive processing. - if (gotEscape) - { - stringBuilder.Append('\\'); - stringBuilder.Append(args[i]); - gotEscape = false; - continue; - } - - // If this characters is not escaped, then look for quotes and - // delimiters. - if (inQuotes || leftCurlies > 0) - { - if (inQuotes && args[i] == quoteChar) - { - // If we're inside quotes, then only an end quote that is not - // escaped is special, and will act as a delimiter. - inQuotes = false; - list.Add(stringBuilder.ToString()); - stringBuilder.Length = 0; - nonWhitespaceFound = false; - } - else - { - if (leftCurlies > 0 && args[i] == '}') - { - leftCurlies--; - } - else if (args[i] == '{') - { - leftCurlies++; - } - stringBuilder.Append(args[i]); - } - } - else - { - if (args[i] == '"' || args[i] == '\'') - { - // If we're not inside quotes, then a start quote can only - // occur as the first non-whitespace character in a name or value. - if (collectionIndexer && i < args.Length - 1 && args[i + 1] == ']') - { - collectionIndexer = false; - stringBuilder.Append(args[i]); - } - else if (i > 0 && args[i - 1] == '[') - { - collectionIndexer = true; - stringBuilder.Append(args[i]); - } - else - { - if (stringBuilder.Length != 0) - return null; - - inQuotes = true; - quoteChar = args[i]; - } - } - else if (args[i] == ',' || args[i] == '=') - { - // If there is something in the stringbuilder, then store it - if (stringBuilder != null && stringBuilder.Length > 0) - { - list.Add(stringBuilder.ToString().Trim()); - stringBuilder.Length = 0; - } - else if (list.Count == 0 || list[list.Count - 1] is Char) - { - // Can't have two delimiters in a row, so check what is on - // the list and complain if the last item is a character, or if - // a delimiter is the first item. - return null; - } - - // Append known delimiters. - list.Add(args[i]); - nonWhitespaceFound = false; - } - else if (args[i] == '}') - { - // If we hit the outside right curly brace, then end processing. If - // there is a delimiter on the top of the stack and we haven't - // hit another non-whitespace character, then its an error - if (stringBuilder != null) - { - if (stringBuilder.Length > 0) - { - list.Add(stringBuilder.ToString().Trim()); - stringBuilder.Length = 0; - } - else if (list.Count > 0 && (list[list.Count - 1] is Char)) - return null; - } - break; - } - else - { - if (args[i] == '{') - { - leftCurlies++; - } - // Must just be a plain old character, so add it to the stringbuilder - stringBuilder.Append(args[i]); - } - } - } - - } - - - // If we've accumulated content but haven't hit a terminating '}' then the - // format is bad, so complain. - if (stringBuilder != null && stringBuilder.Length > 0) - throw new Exception(SR.GetString(SR.Error_MarkupExtensionMissingTerminatingCharacter)); - else if (i < length) - { - // If there is non-whitespace text left that we haven't processes yet, - // then there is junk after the closing '}', so complain - for (++i; i < length; i++) - { - if (!Char.IsWhiteSpace(args[i])) - throw new Exception(SR.GetString(SR.Error_ExtraCharacterFoundAtEnd)); - } - } - - return list; - } - // Remove any '\' escape characters from the passed string. This does a simple // pass through the string and won't do anything if there are no '\' characters. - private void RemoveEscapes(ref string value) + private static void RemoveEscapes(ref string value) { StringBuilder builder = null; bool noEscape = true; @@ -1972,7 +1789,7 @@ private void RemoveEscapes(ref string value) #region ContentProperty Support [ExcludeFromCodeCoverage] - private class ContentProperty : IDisposable + private sealed class ContentProperty : IDisposable { private readonly WorkflowMarkupSerializationManager serializationManager; private readonly WorkflowMarkupSerializer parentObjectSerializer; @@ -1993,58 +1810,79 @@ public ContentProperty(WorkflowMarkupSerializationManager serializationManager, this.contentPropertySerializer = this.serializationManager.GetSerializer(this.contentProperty.PropertyType, typeof(WorkflowMarkupSerializer)) as WorkflowMarkupSerializer; if (this.contentPropertySerializer != null) { + InitializeContentPropertySerializer(serializationManager); + } + else + { + this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerNotAvailableForSerialize, this.contentProperty.PropertyType.FullName))); + } + } + } + + private void InitializeContentPropertySerializer(WorkflowMarkupSerializationManager serializationManager) + { + try + { + XmlReader reader = this.serializationManager.WorkflowMarkupStack[typeof(XmlReader)] as XmlReader; + object contentPropertyValue = null; + if (reader == null) + { + contentPropertyValue = this.contentProperty.GetValue(this.parentObject, null); + } + else if (!this.contentProperty.PropertyType.IsValueType && + !this.contentProperty.PropertyType.IsPrimitive && + this.contentProperty.PropertyType != typeof(string) && + !IsMarkupExtensionType(this.contentProperty.PropertyType) && + this.contentProperty.CanWrite) + { + if (serializationManager.GetSerializer(this.contentProperty.PropertyType, typeof(WorkflowMarkupSerializer)) is not WorkflowMarkupSerializer serializer) + { + serializationManager.ReportError(CreateSerializationError(SR.GetString(SR.Error_SerializerNotAvailable, this.contentProperty.PropertyType.FullName), reader)); + return; + } try { - XmlReader reader = this.serializationManager.WorkflowMarkupStack[typeof(XmlReader)] as XmlReader; - object contentPropertyValue = null; - if (reader == null) - { - contentPropertyValue = this.contentProperty.GetValue(this.parentObject, null); - } - else if (!this.contentProperty.PropertyType.IsValueType && - !this.contentProperty.PropertyType.IsPrimitive && - this.contentProperty.PropertyType != typeof(string) && - !IsMarkupExtension(this.contentProperty.PropertyType) && - this.contentProperty.CanWrite) - { - if (serializationManager.GetSerializer(this.contentProperty.PropertyType, typeof(WorkflowMarkupSerializer)) is not WorkflowMarkupSerializer serializer) - { - serializationManager.ReportError(CreateSerializationError(SR.GetString(SR.Error_SerializerNotAvailable, this.contentProperty.PropertyType.FullName), reader)); - return; - } - try - { - contentPropertyValue = serializer.CreateInstance(serializationManager, this.contentProperty.PropertyType); - } - catch (Exception e) when (!ExceptionUtility.IsCriticalException(e)) - { - serializationManager.ReportError(CreateSerializationError(SR.GetString(SR.Error_SerializerCreateInstanceFailed, this.contentProperty.PropertyType.FullName, e.Message), reader)); - return; - } - this.contentProperty.SetValue(this.parentObject, contentPropertyValue, null); - } - - if (contentPropertyValue != null && reader != null) - { - this.contentPropertySerializer.OnBeforeDeserialize(this.serializationManager, contentPropertyValue); - this.contentPropertySerializer.OnBeforeDeserializeContents(this.serializationManager, contentPropertyValue); - } + contentPropertyValue = serializer.CreateInstance(serializationManager, this.contentProperty.PropertyType); } catch (Exception e) when (!ExceptionUtility.IsCriticalException(e)) { - this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerThrewException, this.parentObject.GetType(), e.Message), e)); + serializationManager.ReportError(CreateSerializationError(SR.GetString(SR.Error_SerializerCreateInstanceFailed, this.contentProperty.PropertyType.FullName, e.Message), reader)); + return; } + this.contentProperty.SetValue(this.parentObject, contentPropertyValue, null); } - else + + if (contentPropertyValue != null && reader != null) { - this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerNotAvailableForSerialize, this.contentProperty.PropertyType.FullName))); + this.contentPropertySerializer.OnBeforeDeserialize(this.serializationManager, contentPropertyValue); + this.contentPropertySerializer.OnBeforeDeserializeContents(this.serializationManager, contentPropertyValue); } } + catch (Exception e) when (!ExceptionUtility.IsCriticalException(e)) + { + this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerThrewException, this.parentObject.GetType(), e.Message), e)); + } } - void IDisposable.Dispose() + private static bool IsMarkupExtensionType(Type type) + { + return (typeof(MarkupExtension).IsAssignableFrom(type) || + typeof(System.Type).IsAssignableFrom(type) || + typeof(System.Array).IsAssignableFrom(type)); + } + + public void Dispose() { - if (this.serializationManager.WorkflowMarkupStack[typeof(XmlReader)] is XmlReader && this.contentProperty != null && this.contentPropertySerializer != null) + Dispose(true); + } + + private bool disposed; + private void Dispose(bool disposing) + { + if (this.disposed) + return; + + if (disposing && this.serializationManager.WorkflowMarkupStack[typeof(XmlReader)] is XmlReader && this.contentProperty != null && this.contentPropertySerializer != null) { try { @@ -2057,6 +1895,8 @@ void IDisposable.Dispose() this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerThrewException, this.parentObject.GetType(), e.Message), e)); } } + + this.disposed = true; } internal PropertyInfo Property @@ -2100,82 +1940,92 @@ internal void SetContents(IList contents) object propertyValue = this.contentProperty.GetValue(this.parentObject, null); if (CollectionMarkupSerializer.IsValidCollectionType(this.contentProperty.PropertyType)) { - if (propertyValue == null) - { - this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_ContentPropertyCanNotBeNull, this.contentProperty.Name, this.parentObject.GetType().FullName))); - return; - } - - //Notify serializer about begining of deserialization process - int i = 0; - try - { - foreach (ContentInfo contentInfo in contents) - { - this.contentPropertySerializer.AddChild(this.serializationManager, propertyValue, contentInfo.Content); - i++; - } - } - catch (Exception e) when (!ExceptionUtility.IsCriticalException(e)) - { - this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerThrewException, this.parentObject.GetType(), e.Message), e, contents[i].LineNumber, contents[i].LinePosition)); - } + SetPropertyValueForCollection(contents, propertyValue); } else { - if (!this.contentProperty.CanWrite) - { - this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_ContentPropertyNoSetter, this.contentProperty.Name, this.parentObject.GetType()), contents[0].LineNumber, contents[0].LinePosition)); - return; - } + SetPropertyValueForObject(contents); + } + } + } - if (contents.Count > 1) - this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_ContentPropertyNoMultipleContents, this.contentProperty.Name, this.parentObject.GetType()), contents[1].LineNumber, contents[1].LinePosition)); + private void SetPropertyValueForCollection(IList contents, object propertyValue) + { + if (propertyValue == null) + { + this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_ContentPropertyCanNotBeNull, this.contentProperty.Name, this.parentObject.GetType().FullName))); + return; + } - object content = contents[0].Content; - if (!this.contentProperty.PropertyType.IsAssignableFrom(content.GetType()) && typeof(string).IsAssignableFrom(content.GetType())) - { - try - { - content = this.contentPropertySerializer.DeserializeFromString(this.serializationManager, this.contentProperty.PropertyType, content as string); - content = WorkflowMarkupSerializer.GetValueFromMarkupExtension(this.serializationManager, content); - } - catch (Exception e) when (!ExceptionUtility.IsCriticalException(e)) - { - this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerThrewException, this.parentObject.GetType(), e.Message), e, contents[0].LineNumber, contents[0].LinePosition)); - return; - } - } + //Notify serializer about begining of deserialization process + int i = 0; + try + { + foreach (ContentInfo contentInfo in contents) + { + this.contentPropertySerializer.AddChild(this.serializationManager, propertyValue, contentInfo.Content); + i++; + } + } + catch (Exception e) when (!ExceptionUtility.IsCriticalException(e)) + { + this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerThrewException, this.parentObject.GetType(), e.Message), e, contents[i].LineNumber, contents[i].LinePosition)); + } + } - if (content == null) - { - this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_ContentCanNotBeConverted, content as string, contentProperty.Name, this.parentObject.GetType().FullName, this.contentProperty.PropertyType.FullName), contents[0].LineNumber, contents[0].LinePosition)); - } - else if (!contentProperty.PropertyType.IsAssignableFrom(content.GetType())) - { - this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_ContentPropertyValueInvalid, content.GetType(), this.contentProperty.Name, this.contentProperty.PropertyType.FullName), contents[0].LineNumber, contents[0].LinePosition)); - } - else + private void SetPropertyValueForObject(IList contents) + { + if (!this.contentProperty.CanWrite) + { + this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_ContentPropertyNoSetter, this.contentProperty.Name, this.parentObject.GetType()), contents[0].LineNumber, contents[0].LinePosition)); + return; + } + + if (contents.Count > 1) + this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_ContentPropertyNoMultipleContents, this.contentProperty.Name, this.parentObject.GetType()), contents[1].LineNumber, contents[1].LinePosition)); + + object content = contents[0].Content; + if (!this.contentProperty.PropertyType.IsInstanceOfType(content) && content is string contentString) + { + try + { + content = this.contentPropertySerializer.DeserializeFromString(this.serializationManager, this.contentProperty.PropertyType, contentString); + content = WorkflowMarkupSerializer.GetValueFromMarkupExtension(this.serializationManager, content); + } + catch (Exception e) when (!ExceptionUtility.IsCriticalException(e)) + { + this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerThrewException, this.parentObject.GetType(), e.Message), e, contents[0].LineNumber, contents[0].LinePosition)); + return; + } + } + + if (content == null) + { + this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_ContentCanNotBeConverted, content as string, contentProperty.Name, this.parentObject.GetType().FullName, this.contentProperty.PropertyType.FullName), contents[0].LineNumber, contents[0].LinePosition)); + } + else if (!contentProperty.PropertyType.IsInstanceOfType(content)) + { + this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_ContentPropertyValueInvalid, content.GetType(), this.contentProperty.Name, this.contentProperty.PropertyType.FullName), contents[0].LineNumber, contents[0].LinePosition)); + } + else + { + try + { + if (this.contentProperty.PropertyType == typeof(string)) { - try - { - if (this.contentProperty.PropertyType == typeof(string)) - { - content = new WorkflowMarkupSerializer().DeserializeFromString(this.serializationManager, this.contentProperty.PropertyType, content as string); - content = WorkflowMarkupSerializer.GetValueFromMarkupExtension(this.serializationManager, content); - } - this.contentProperty.SetValue(this.parentObject, content, null); - } - catch (Exception e) when (!ExceptionUtility.IsCriticalException(e)) - { - this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerThrewException, this.parentObject.GetType(), e.Message), e, contents[0].LineNumber, contents[0].LinePosition)); - } + content = new WorkflowMarkupSerializer().DeserializeFromString(this.serializationManager, this.contentProperty.PropertyType, content as string); + content = WorkflowMarkupSerializer.GetValueFromMarkupExtension(this.serializationManager, content); } + this.contentProperty.SetValue(this.parentObject, content, null); + } + catch (Exception e) when (!ExceptionUtility.IsCriticalException(e)) + { + this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerThrewException, this.parentObject.GetType(), e.Message), e, contents[0].LineNumber, contents[0].LinePosition)); } } } - private PropertyInfo GetContentProperty(WorkflowMarkupSerializationManager serializationManagerLocal, object parentObjectLocal) + private static PropertyInfo GetContentProperty(WorkflowMarkupSerializationManager serializationManagerLocal, object parentObjectLocal) { PropertyInfo contentPropertyLocal = null; @@ -2224,13 +2074,6 @@ internal static string EnsureMarkupExtensionTypeName(XmlQualifiedName xmlQualifi return typeName; } - private static bool IsMarkupExtension(Type type) - { - return (typeof(MarkupExtension).IsAssignableFrom(type) || - typeof(System.Type).IsAssignableFrom(type) || - typeof(System.Array).IsAssignableFrom(type)); - } - private static bool IsMarkupExtension(XmlQualifiedName xmlQualifiedName) { bool markupExtension = false; @@ -2246,10 +2089,10 @@ private static object GetMarkupExtensionFromValue(object value) { if (value == null) return new NullExtension(); - if (value is System.Type) - return new TypeExtension(value as System.Type); - if (value is Array) - return new ArrayExtension(value as Array); + if (value is System.Type typeValue) + return new TypeExtension(typeValue); + if (value is Array arrayValue) + return new ArrayExtension(arrayValue); return value; } diff --git a/LogicBuilder.Workflow.ComponentModel.Serialization/LogicBuilder.Workflow.ComponentModel.Serialization.csproj b/LogicBuilder.Workflow.ComponentModel.Serialization/LogicBuilder.Workflow.ComponentModel.Serialization.csproj index 69c81b2..580f458 100644 --- a/LogicBuilder.Workflow.ComponentModel.Serialization/LogicBuilder.Workflow.ComponentModel.Serialization.csproj +++ b/LogicBuilder.Workflow.ComponentModel.Serialization/LogicBuilder.Workflow.ComponentModel.Serialization.csproj @@ -31,7 +31,7 @@ - + @@ -70,7 +70,6 @@ - diff --git a/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/AttributesTokenizerTest.cs b/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/AttributesTokenizerTest.cs new file mode 100644 index 0000000..9281878 --- /dev/null +++ b/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/AttributesTokenizerTest.cs @@ -0,0 +1,632 @@ +using LogicBuilder.Workflow.ComponentModel.Serialization; +using System; +using System.Collections; + +namespace LogicBuilder.Workflow.Tests.ComponentModel.Serialization +{ + public class AttributesTokenizerTest + { + private readonly AttributesTokenizer _tokenizer; + + public AttributesTokenizerTest() + { + _tokenizer = new AttributesTokenizer(); + } + + #region Basic Tokenization Tests + + [Fact] + public void TokenizeAttributes_ReturnsNull_WhenInputIsEmpty() + { + // Arrange + string input = ""; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.Null(result); + } + + [Fact] + public void TokenizeAttributes_ReturnsNull_WhenInputIsWhitespaceOnly() + { + // Arrange + string input = " "; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.Null(result); + } + + [Fact] + public void TokenizeAttributes_ReturnsSingleToken_WhenInputIsSingleValue() + { + // Arrange + string input = "value}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("value", result[0]); + } + + [Fact] + public void TokenizeAttributes_TokenizesMultipleValues_WithCommaDelimiter() + { + // Arrange + string input = "value1,value2,value3}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Equal(5, result.Count); + Assert.Equal("value1", result[0]); + Assert.Equal(',', result[1]); + Assert.Equal("value2", result[2]); + Assert.Equal(',', result[3]); + Assert.Equal("value3", result[4]); + } + + [Fact] + public void TokenizeAttributes_TokenizesKeyValuePair_WithEqualsDelimiter() + { + // Arrange + string input = "key=value}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Equal(3, result.Count); + Assert.Equal("key", result[0]); + Assert.Equal('=', result[1]); + Assert.Equal("value", result[2]); + } + + #endregion + + #region Quote Handling Tests + + [Fact] + public void TokenizeAttributes_HandlesDoubleQuotedString() + { + // Arrange + string input = "\"quoted value\"}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("quoted value", result[0]); + } + + [Fact] + public void TokenizeAttributes_HandlesSingleQuotedString() + { + // Arrange + string input = "'quoted value'}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("quoted value", result[0]); + } + + [Fact] + public void TokenizeAttributes_HandlesQuotedStringWithComma() + { + // Arrange + string input = "\"value,with,commas\"}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("value,with,commas", result[0]); + } + + [Fact] + public void TokenizeAttributes_HandlesQuotedStringWithEquals() + { + // Arrange + string input = "\"value=with=equals\"}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("value=with=equals", result[0]); + } + + [Fact] + public void TokenizeAttributes_HandlesMultipleQuotedStrings() + { + // Arrange + string input = "\"first\",\"second\",\"third\"}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Equal(5, result.Count); + Assert.Equal("first", result[0]); + Assert.Equal(',', result[1]); + Assert.Equal("second", result[2]); + Assert.Equal(',', result[3]); + Assert.Equal("third", result[4]); + } + + [Fact] + public void TokenizeAttributes_ReturnsEmptyList_WhenQuoteNotAtBeginning() + { + // Arrange + string input = "value\"quoted\"}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + } + + #endregion + + #region Escape Character Tests + + [Fact] + public void TokenizeAttributes_HandlesEscapedBackslash() + { + // Arrange + string input = "value\\\\test}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("value\\\\test", result[0]); + } + + [Fact] + public void TokenizeAttributes_HandlesEscapedComma() + { + // Arrange + string input = "value\\,test}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("value\\,test", result[0]); + } + + [Fact] + public void TokenizeAttributes_HandlesEscapedEquals() + { + // Arrange + string input = "value\\=test}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("value\\=test", result[0]); + } + + [Fact] + public void TokenizeAttributes_HandlesEscapedQuote() + { + // Arrange + string input = "\\\"value}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("\\\"value", result[0]); + } + + #endregion + + #region Curly Brace Tests + + [Fact] + public void TokenizeAttributes_HandlesNestedCurlyBraces() + { + // Arrange + string input = "Stuff {nested {content}}}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("Stuff {nested {content}}", result[0]); + } + + [Fact] + public void TokenizeAttributes_HandlesMultipleLevelsOfNesting() + { + // Arrange + string input = "Stuff {level1 {level2 {level3}}}}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("Stuff {level1 {level2 {level3}}}", result[0]); + } + + [Fact] + public void TokenizeAttributes_HandlesNestedBracesWithCommas() + { + // Arrange + string input = "{nested,content},value}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Equal(3, result.Count); + Assert.Equal("{nested,content}", result[0]); + Assert.Equal(',', result[1]); + Assert.Equal("value", result[2]); + } + + #endregion + + #region Collection Indexer Tests + + [Fact] + public void TokenizeAttributes_HandlesCollectionIndexerWithSingleQuotedIndex() + { + // Arrange + string input = "collection['someName']}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("collection['someName']", result[0]); + } + + [Fact] + public void TokenizeAttributes_HandlesCollectionIndexerWithDoubleQuotedIndex() + { + // Arrange + string input = "collection[\"someName\"]}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("collection[\"someName\"]", result[0]); + } + + [Fact] + public void TokenizeAttributes_HandlesCollectionIndexerWithSingleQuote() + { + // Arrange + string input = "collection[']']}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("collection[']']", result[0]); + } + + [Fact] + public void TokenizeAttributes_HandlesCollectionIndexerWithDoubleQuote() + { + // Arrange + string input = "collection[\"]\"]}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("collection[\"]\"]", result[0]); + } + + [Fact] + public void TokenizeAttributes_ReturnsEmptyCollectionUsingSingleQuote_WithMissingClosingSquareBracket() + { + // Arrange + string input = "collection['A'}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + } + + [Fact] + public void TokenizeAttributes_ReturnsEmptyCollectionUsingDoubleQuote_WithMissingClosingSquareBracket() + { + // Arrange + string input = "collection[\"A\"}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + } + + [Fact] + public void TokenizeAttributes_HandlesQuoteAfterOpenBracket() + { + // Arrange + string input = "array['key']}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("array['key']", result[0]); + } + + #endregion + + #region Whitespace Handling Tests + + [Fact] + public void TokenizeAttributes_TrimsWhitespaceFromValues() + { + // Arrange + string input = " value1 , value2 }"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Equal(3, result.Count); + Assert.Equal("value1", result[0]); + Assert.Equal(',', result[1]); + Assert.Equal("value2", result[2]); + } + + [Fact] + public void TokenizeAttributes_PreservesWhitespaceInQuotes() + { + // Arrange + string input = "\" value with spaces \"}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal(" value with spaces ", result[0]); + } + + #endregion + + #region Error Handling Tests + + [Fact] + public void TokenizeAttributes_ReturnsEmptyList_WhenTwoDelimitersInRow() + { + // Arrange + string input = "value1,,value2}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + } + + [Fact] + public void TokenizeAttributes_ReturnsEmptyList_WhenDelimiterIsFirst() + { + // Arrange + string input = ",value}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + } + + [Fact] + public void TokenizeAttributes_ReturnsEmptyList_WhenDelimiterBeforeClosingBrace() + { + // Arrange + string input = "value,}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + } + + [Fact] + public void TokenizeAttributes_ThrowsException_WhenMissingTerminatingCharacter() + { + // Arrange + string input = "value"; + + // Act & Assert + Assert.Throws(() => _tokenizer.TokenizeAttributes(input)); + } + + [Fact] + public void TokenizeAttributes_ThrowsException_WhenExtraCharactersAfterClosingBrace() + { + // Arrange + string input = "value}extra"; + + // Act & Assert + Assert.Throws(() => _tokenizer.TokenizeAttributes(input)); + } + + [Fact] + public void TokenizeAttributes_DoesNotThrow_WhenWhitespaceAfterClosingBrace() + { + // Arrange + string input = "value} "; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("value", result[0]); + } + + #endregion + + #region Complex Scenario Tests + + [Fact] + public void TokenizeAttributes_HandlesComplexMarkupExtension() + { + // Arrange + string input = "PropertyName={StaticResource Key},PropertyValue=\"test\"}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Equal(7, result.Count); + Assert.Equal("PropertyName", result[0]); + Assert.Equal('=', result[1]); + Assert.Equal("{StaticResource Key}", result[2]); + Assert.Equal(',', result[3]); + Assert.Equal("PropertyValue", result[4]); + Assert.Equal('=', result[5]); + Assert.Equal("test", result[6]); + } + + [Fact] + public void TokenizeAttributes_HandlesMixedQuotesAndBraces() + { + // Arrange + string input = "\"quoted\",{nested},unquoted}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Equal(5, result.Count); + Assert.Equal("quoted", result[0]); + Assert.Equal(',', result[1]); + Assert.Equal("{nested}", result[2]); + Assert.Equal(',', result[3]); + Assert.Equal("unquoted", result[4]); + } + + [Fact] + public void TokenizeAttributes_HandlesMultipleKeyValuePairs() + { + // Arrange + string input = "key1=value1,key2=value2,key3=value3}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Equal(11, result.Count); + Assert.Equal("key1", result[0]); + Assert.Equal('=', result[1]); + Assert.Equal("value1", result[2]); + Assert.Equal(',', result[3]); + Assert.Equal("key2", result[4]); + Assert.Equal('=', result[5]); + Assert.Equal("value2", result[6]); + Assert.Equal(',', result[7]); + Assert.Equal("key3", result[8]); + Assert.Equal('=', result[9]); + Assert.Equal("value3", result[10]); + } + + [Fact] + public void TokenizeAttributes_HandlesEscapedCharactersInQuotedString() + { + // Arrange + string input = "\"{nested\\}} content\"}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("{nested\\}} content", result[0]); + } + + [Fact] + public void TokenizeAttributes_HandlesNestedMarkupExtensionsWithMultipleParameters() + { + // Arrange + string input = "Stuff {Binding Path=Property,Source={StaticResource ResourceKey}}}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("Stuff {Binding Path=Property,Source={StaticResource ResourceKey}}", result[0]); + } + + #endregion + } +} \ No newline at end of file diff --git a/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/IFromCompactFormatDeserializer.cs b/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/IFromCompactFormatDeserializer.cs new file mode 100644 index 0000000..621d054 --- /dev/null +++ b/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/IFromCompactFormatDeserializer.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LogicBuilder.Workflow.Tests.ComponentModel.Serialization +{ + internal interface IFromCompactFormatDeserializer + { + object Deserialize(object value); + } +} diff --git a/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/MarkupExtensionSerializerTest.cs b/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/MarkupExtensionSerializerTest.cs index a3feb0d..9be5e89 100644 --- a/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/MarkupExtensionSerializerTest.cs +++ b/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/MarkupExtensionSerializerTest.cs @@ -82,7 +82,7 @@ public void SerializeToString_ThrowsArgumentNullException_WhenXmlWriterIsNull() var markupExtension = new TestMarkupExtension(); // Act & Assert - Assert.Throws(() => _serializer.SerializeToString(_serializationManager, markupExtension)); + Assert.Throws(() => _serializer.SerializeToString(_serializationManager, markupExtension)); } [Fact] @@ -370,46 +370,6 @@ public void SerializeToString_HandlesTypeConstructorArguments() } } - [Fact] - public void SerializeToString_SkipsPropertiesWithConstructorArgumentAttribute() - { - // Arrange - var designerSerializationManager = new DesignerSerializationManager(); - var manager = new WorkflowMarkupSerializationManager(designerSerializationManager); - - var serializer = new TestableMarkupExtensionSerializerWithConstructor(); - var markupExtension = new TestMarkupExtensionWithConstructor("testValue"); - var sb = new StringBuilder(); - var settings = new XmlWriterSettings { OmitXmlDeclaration = true }; - using var writer = XmlWriter.Create(sb, settings); - manager.WorkflowMarkupStack.Push(writer); - - try - { - writer.WriteStartElement("", "Root", "http://schemas.microsoft.com/winfx/2006/xaml"); - writer.WriteAttributeString("xmlns", "ns0", null, "clr-namespace:LogicBuilder.Workflow.Tests.ComponentModel.Serialization;Assembly=LogicBuilder.Workflow.ComponentModel.Serialization.Tests, Version=1.0.0.0, Culture=neutral, PublicKeyToken=646893bec0268535"); - - using (designerSerializationManager.CreateSession()) - { - // Act - serializer.SerializeToString(manager, markupExtension); - } - - writer.WriteEndElement(); - writer.Flush(); - - // Assert - Value property should not appear as a property since it's a constructor argument - var result = sb.ToString(); - // The value should appear as constructor arg, not as property=value syntax - Assert.Contains("testValue", result); - } - finally - { - manager.WorkflowMarkupStack.Pop(); - writer.Dispose(); - } - } - [Fact] public void SerializeToString_HandlesIntConstructorArguments() { @@ -671,7 +631,7 @@ public string TestCreateEscapedValue(string value) { // Use reflection to access private method var method = typeof(MarkupExtensionSerializer).GetMethod("CreateEscapedValue", - BindingFlags.NonPublic | BindingFlags.Instance); + BindingFlags.NonPublic | BindingFlags.Static); return (string)method!.Invoke(this, [value])!; } } diff --git a/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/WorkflowMarkupSerializationExceptionTest.cs b/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/WorkflowMarkupSerializationExceptionTest.cs index 35bd97b..7062eaa 100644 --- a/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/WorkflowMarkupSerializationExceptionTest.cs +++ b/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/WorkflowMarkupSerializationExceptionTest.cs @@ -1,9 +1,5 @@ using LogicBuilder.Workflow.ComponentModel.Serialization; using System; -using System.IO; -using System.Runtime.Serialization; -using System.Runtime.Serialization.Formatters.Binary; -using Xunit; namespace LogicBuilder.Workflow.Tests.ComponentModel.Serialization { diff --git a/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/WorkflowMarkupSerializerTest.cs b/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/WorkflowMarkupSerializerTest.cs index 158c939..c4cfa9d 100644 --- a/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/WorkflowMarkupSerializerTest.cs +++ b/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/WorkflowMarkupSerializerTest.cs @@ -1,5 +1,6 @@ using LogicBuilder.ComponentModel.Design.Serialization; using LogicBuilder.Workflow.ComponentModel.Serialization; +using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.ComponentModel; @@ -688,7 +689,10 @@ public void SerializeToString_WithDelegate_ReturnsMethodName() Assert.Equal("TestMethod", result); } - private void TestMethod() { } + private void TestMethod() + { + // Method intentionally left empty. + } #endregion @@ -1403,7 +1407,7 @@ public void Deserialize_WithIntValue_DeserializesObject() [Fact] public void DeserializeFromCompactFormat_WithNullExtension_CreatesNullExtension() { - // Arrange + // Arrange PropertyName={StaticResource Key},PropertyValue=\"test\"} var serializer = new WorkflowMarkupSerializer(); var xml = ""; using var reader = XmlReader.Create(new StringReader(xml)); @@ -1509,18 +1513,6 @@ public void Serialize_WithArrayValue_SerializesAsArrayExtension() #endregion - #region Serialization Error Tests - - [Fact] - public void Serialize_WithCircularReference_ThrowsException() - { - // This would test circular reference detection - // The implementation should detect and prevent infinite loops - Assert.True(true, "Circular reference test placeholder - implement circular reference detection in the serializer to pass this test."); - } - - #endregion - #region Additional Coverage Tests [Fact] @@ -2046,5 +2038,632 @@ public void Serialize_WithComplexObject_SerializesContents() } #endregion + + #region TokenizeAttributes Error Cases Tests + + [Fact] + public void DeserializeFromCompactFormat_WithMissingTerminatingBrace_ReportsError() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var xml = ""; + using var reader = XmlReader.Create(new StringReader(xml)); + reader.Read(); + var manager = new DesignerSerializationManager(); + var value = "{x:Null arg1, arg2"; + + // Act + object result; + using (manager.CreateSession()) + { + var wfManager = new WorkflowMarkupSerializationManager(manager); + wfManager.WorkflowMarkupStack.Push(reader); + result = serializer.DeserializeFromCompactFormat(wfManager, reader, value); + wfManager.WorkflowMarkupStack.Pop(); + } + + // Assert - Should report error for missing terminating brace + Assert.Null(result); + } + + [Fact] + public void DeserializeFromCompactFormat_WithParameterToParameterlessExtension_ReportsError() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var xml = ""; + using var reader = XmlReader.Create(new StringReader(xml)); + reader.Read(); + var manager = new DesignerSerializationManager(); + var value = "{x:Null arg1}"; + + // Act + object result; + using (manager.CreateSession()) + { + var wfManager = new WorkflowMarkupSerializationManager(manager); + wfManager.WorkflowMarkupStack.Push(reader); + result = serializer.DeserializeFromCompactFormat(wfManager, reader, value); + wfManager.WorkflowMarkupStack.Pop(); + } + + // Assert - Should report error for trailing delimiter + Assert.Null(result); + } + + [Fact] + public void DeserializeFromCompactFormat_WithDoubleQuotes_ParsesCorrectly() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var xml = ""; + using var reader = XmlReader.Create(new StringReader(xml)); + reader.Read(); + var manager = new DesignerSerializationManager(); + var value = "{x:Type \"System.String\"}"; + + // Act + object? result; + using (manager.CreateSession()) + { + var wfManager = new WorkflowMarkupSerializationManager(manager); + wfManager.WorkflowMarkupStack.Push(reader); + result = serializer.DeserializeFromCompactFormat(wfManager, reader, value); + wfManager.WorkflowMarkupStack.Pop(); + } + + // Assert + Assert.NotNull(result); + } + + [Fact] + public void DeserializeFromCompactFormat_WithNestedCurlyBraces_ParsesCorrectly() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var xml = ""; + using var reader = XmlReader.Create(new StringReader(xml)); + reader.Read(); + var manager = new DesignerSerializationManager(); + var value = "{x:Null}"; + + // Act + object? result; + using (manager.CreateSession()) + { + var wfManager = new WorkflowMarkupSerializationManager(manager); + wfManager.WorkflowMarkupStack.Push(reader); + result = serializer.DeserializeFromCompactFormat(wfManager, reader, value); + wfManager.WorkflowMarkupStack.Pop(); + } + + // Assert + Assert.IsType(result); + } + + [Fact] + public void DeserializeFromCompactFormat_WithExtraCharactersAfterBrace_ReportsError() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var xml = ""; + using var reader = XmlReader.Create(new StringReader(xml)); + reader.Read(); + var manager = new DesignerSerializationManager(); + var value = "{x:Null} extra"; + + // Act + object? result; + using (manager.CreateSession()) + { + var wfManager = new WorkflowMarkupSerializationManager(manager); + wfManager.WorkflowMarkupStack.Push(reader); + result = serializer.DeserializeFromCompactFormat(wfManager, reader, value); + wfManager.WorkflowMarkupStack.Pop(); + } + + // Assert - should handle extra characters + Assert.Null(result); + } + + #endregion + + #region CreateInstance Coverage Tests + + public class TestClassWithoutDefaultConstructor(string value) + { + public string Value { get; } = value; + } + + [Fact] + public void CreateInstance_WithNullSerializationManager_ThrowsArgumentNullException() + { + // Arrange + var serializer = new TestWorkflowMarkupSerializer(); + var type = typeof(TestSimpleObject); + + // Act & Assert + Assert.Throws(() => serializer.CallCreateInstance(null!, type)); + } + + [Fact] + public void CreateInstance_WithNullType_ThrowsArgumentNullException() + { + // Arrange + var serializer = new TestWorkflowMarkupSerializer(); + var manager = new DesignerSerializationManager(); + + // Act & Assert + using (manager.CreateSession()) + { + Assert.Throws(() => + serializer.CallCreateInstance(new WorkflowMarkupSerializationManager(manager), null!)); + } + } + + [Fact] + public void CreateInstance_WithValidType_CreatesInstance() + { + // Arrange + var serializer = new TestWorkflowMarkupSerializer(); + var manager = new DesignerSerializationManager(); + var type = typeof(TestSimpleObject); + + // Act + object result; + using (manager.CreateSession()) + { + result = serializer.CallCreateInstance(new WorkflowMarkupSerializationManager(manager), type); + } + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + // Helper class to expose protected CreateInstance method + private class TestWorkflowMarkupSerializer : WorkflowMarkupSerializer + { + public object CallCreateInstance(WorkflowMarkupSerializationManager manager, Type type) + { + return CreateInstance(manager, type); + } + } + + #endregion + + #region SerializeContents Events Tests + + public class TestObjectWithEvents + { + public event EventHandler? TestEvent; + public string Name { get; set; } = string.Empty; + + public void RaiseTestEvent() + { + TestEvent?.Invoke(this, EventArgs.Empty); + } + } + + [Fact] + public void Serialize_WithEvents_SerializesCorrectly() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var output = new StringWriter(); + using var writer = XmlWriter.Create(output); + var obj = new TestObjectWithEvents { Name = "Test" }; + + // Act + serializer.Serialize(writer, obj); + writer.Flush(); + var result = output.ToString(); + + // Assert + Assert.NotEmpty(result); + } + + #endregion + + #region SerializeContents Properties with Index Parameters Tests + + public class TestObjectWithIndexer + { + private readonly Dictionary _values = []; + + public string this[string key] + { + get => _values.TryGetValue(key, out var value) ? value : string.Empty; + set => _values[key] = value; + } + + public string Name { get; set; } = string.Empty; + } + + [Fact] + public void Serialize_WithIndexedProperty_SkipsIndexedProperty() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var output = new StringWriter(); + using var writer = XmlWriter.Create(output); + var obj = new TestObjectWithIndexer { Name = "Test" }; + obj["key1"] = "value1"; + + // Act + serializer.Serialize(writer, obj); + writer.Flush(); + var result = output.ToString(); + + // Assert + Assert.Contains("Test", result); + } + + #endregion + + #region ContentProperty Error Path Tests + + [ContentProperty("Items")] + public class TestCollectionContentProperty + { + public TestCollectionContentProperty() + { + Items = []; + } + + public List Items { get; } + } + + [ContentProperty("Items")] + public class TestNullCollectionContentProperty + { + public List? Items { get; set; } + } + + [ContentProperty("Value")] + public class TestReadOnlyContentProperty + { + public string? Value { get; } + } + + [ContentProperty("Value")] + public class TestSingleValueContentProperty + { + public string Value { get; set; } = string.Empty; + } + + #endregion + + #region Circular Reference Detection Tests + + public class TestCircularReference + { + public TestCircularReference? Child { get; set; } + public string Name { get; set; } = string.Empty; + } + + [Fact] + public void Serialize_WithCircularReference_ThrowsWorkflowMarkupSerializationException() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var output = new StringWriter(); + using var writer = XmlWriter.Create(output); + var obj = new TestCircularReference { Name = "Parent" }; + obj.Child = obj; // Create circular reference + + // Act & Assert + // The serializer should detect the circular reference during serialization + // This is handled by the SerializationStack.Contains check + Assert.Throws(() => serializer.Serialize(writer, obj)); + } + + #endregion + + #region OnBeforeSerialize, OnAfterSerialize, OnBeforeDeserialize, OnAfterDeserialize Tests + + #endregion + + #region ShouldSerializeValue with Convertible Types Tests + + public class TestConvertibleProperty + { + [DefaultValue("42")] + public string StringValue { get; set; } = "42"; + } + + [Fact] + public void ShouldSerializeValue_WithConvertibleDefaultValue_ReturnsFalse() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var manager = new DesignerSerializationManager(); + var property = typeof(TestConvertibleProperty).GetProperty("StringValue"); + + // Act + bool result; + using (manager.CreateSession()) + { + var wfManager = new WorkflowMarkupSerializationManager(manager); + wfManager.Context.Push(property!); + result = serializer.ShouldSerializeValue(wfManager, "42"); + wfManager.Context.Pop(); + } + + // Assert + Assert.False(result); + } + + #endregion + + #region SafeXmlNodeWriter Edge Cases Tests + + public class TestPropertyWithSpecialCharacters + { + public string NormalProperty { get; set; } = string.Empty; + } + + [Fact] + public void Serialize_WithSpecialCharactersInPropertyValue_EncodesCorrectly() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var output = new StringWriter(); + using var writer = XmlWriter.Create(output); + var obj = new TestPropertyWithSpecialCharacters { NormalProperty = "<>&\"'" }; + + // Act + serializer.Serialize(writer, obj); + writer.Flush(); + var result = output.ToString(); + + // Assert + Assert.NotEmpty(result); + } + + #endregion + + #region AdvanceReader Coverage Tests + + [Fact] + public void Deserialize_WithComments_SkipsComments() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var xml = @" + +42"; + using var reader = XmlReader.Create(new StringReader(xml)); + + // Act + var result = serializer.Deserialize(reader); + + // Assert + Assert.Equal(42, result); + } + + [Fact] + public void Deserialize_WithWhitespace_SkipsWhitespace() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var xml = @" + + +42"; + using var reader = XmlReader.Create(new StringReader(xml), new XmlReaderSettings { IgnoreWhitespace = false }); + + // Act + var result = serializer.Deserialize(reader); + + // Assert + Assert.Equal(42, result); + } + + #endregion + + #region GetClrFullName Coverage Tests + + [Fact] + public void Deserialize_WithUnmappedNamespace_UsesFullQualifiedName_ReturnsNull() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + // Use a namespace that is not mapped + var xml = @" +Test"; + using var reader = XmlReader.Create(new StringReader(xml)); + + var result = serializer.Deserialize(reader); + Assert.Null(result); + } + + #endregion + + #region DeserializeCompoundProperty Edge Cases Tests + + public class TestObjectWithComplexProperty + { + public TestSimpleObject? ComplexProperty { get; set; } + } + + [Fact] + public void Deserialize_WithEmptyComplexProperty_HandlesCorrectly() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var xml = @" + + +"; + using var reader = XmlReader.Create(new StringReader(xml)); + + // Act & Assert + var result = serializer.Deserialize(reader); + Assert.Null(result); + } + + #endregion + + #region String Escaping Tests + + [Fact] + public void Serialize_WithNullCharInString_ReplacesWithSpace() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var output = new StringWriter(); + using var writer = XmlWriter.Create(output); + var obj = "Test\0Value"; + + // Act + serializer.Serialize(writer, obj); + writer.Flush(); + var result = output.ToString(); + + // Assert + Assert.Contains("Test Value", result); + } + + [Fact] + public void SerializeToString_WithByte_ReturnsString() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var manager = new DesignerSerializationManager(); + byte value = 255; + + // Act + string result; + using (manager.CreateSession()) + { + result = serializer.SerializeToString(new WorkflowMarkupSerializationManager(manager), value); + } + + // Assert + Assert.Equal("255", result); + } + + #endregion + + #region GetProperties and GetEvents Coverage Tests + + public class TestObjectWithHiddenProperty + { + [System.ComponentModel.DesignerSerializationVisibility(System.ComponentModel.DesignerSerializationVisibility.Hidden)] + public string HiddenProperty { get; set; } = string.Empty; + + public string VisibleProperty { get; set; } = string.Empty; + } + + [Fact] + public void GetProperties_WithHiddenProperty_ExcludesHiddenProperty() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var manager = new DesignerSerializationManager(); + var obj = new TestObjectWithHiddenProperty(); + + // Act + PropertyInfo[] result; + using (manager.CreateSession()) + { + result = serializer.GetProperties(new WorkflowMarkupSerializationManager(manager), obj); + } + + // Assert + Assert.DoesNotContain(result, p => p.Name == "HiddenProperty"); + Assert.Contains(result, p => p.Name == "VisibleProperty"); + } + + public class TestObjectWithHiddenEvent + { + [System.ComponentModel.DesignerSerializationVisibility(System.ComponentModel.DesignerSerializationVisibility.Hidden)] + public event EventHandler? HiddenEvent; + + public event EventHandler? VisibleEvent; + + public void RaiseEvents() + { + HiddenEvent?.Invoke(this, EventArgs.Empty); + VisibleEvent?.Invoke(this, EventArgs.Empty); + } + } + + [Fact] + public void GetEvents_WithHiddenEvent_ExcludesHiddenEvent() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var manager = new DesignerSerializationManager(); + var obj = new TestObjectWithHiddenEvent(); + + // Act + EventInfo[] result; + using (manager.CreateSession()) + { + result = serializer.GetEvents(new WorkflowMarkupSerializationManager(manager), obj); + } + + // Assert + Assert.DoesNotContain(result, e => e.Name == "HiddenEvent"); + Assert.Contains(result, e => e.Name == "VisibleEvent"); + } + + #endregion + + #region Additional Primitive Type Serialization Tests + + [Fact] + public void Serialize_WithInt16Value_GeneratesXml() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + short obj = 32767; + var output = new StringWriter(); + using var writer = XmlWriter.Create(output); + + // Act + serializer.Serialize(writer, obj); + writer.Flush(); + var result = output.ToString(); + + // Assert + Assert.Contains("32767", result); + } + + [Fact] + public void Deserialize_WithByteValue_DeserializesCorrectly() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var xml = @" +255"; + using var reader = XmlReader.Create(new StringReader(xml)); + + // Act + var result = serializer.Deserialize(reader); + + // Assert + Assert.Equal((byte)255, result); + } + + [Fact] + public void Deserialize_WithInt16Value_DeserializesCorrectly() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var xml = @" +32767"; + using var reader = XmlReader.Create(new StringReader(xml)); + + // Act + var result = serializer.Deserialize(reader); + + // Assert + Assert.Equal((short)32767, result); + } + + #endregion } } \ No newline at end of file