//----------------------------------------------------------------------------- // Copyright (c) Microsoft Corporation. All rights reserved. //----------------------------------------------------------------------------- namespace System.Activities { using System.Activities.XamlIntegration; using System.ComponentModel; using System.Globalization; using System.Runtime; using System.Runtime.Serialization; using System.Text; using System.Text.RegularExpressions; [DataContract] [Serializable] [TypeConverter(typeof(WorkflowIdentityConverter))] public class WorkflowIdentity : IEquatable { static Regex identityString = new Regex( @"^(?[^;]*) (; (\s* Version \s* = \s* (?[^;]*))? )? (; (\s* Package \s* = \s* (?.*))? )? $", RegexOptions.IgnorePatternWhitespace); const string versionString = "; Version="; const string packageString = "; Package="; string name; Version version; string package; public WorkflowIdentity() { this.name = string.Empty; } public WorkflowIdentity(string name, Version version, string package) { this.name = ValidateName(name, "name"); this.version = version; this.package = ValidatePackage(package, "package"); } public string Name { get { return this.name; } set { this.name = ValidateName(value, "value"); } } public Version Version { get { return this.version; } set { this.version = value; } } public string Package { get { return this.package; } set { this.package = ValidatePackage(value, "value"); } } public static WorkflowIdentity Parse(string identity) { if (identity == null) { throw FxTrace.Exception.ArgumentNull("identity"); } return IdentityParser.Parse(identity, true); } public static bool TryParse(string identity, out WorkflowIdentity result) { if (identity == null) { result = null; return false; } result = IdentityParser.Parse(identity, false); return result != null; } public override bool Equals(object obj) { return Equals(obj as WorkflowIdentity); } public bool Equals(WorkflowIdentity other) { return !object.ReferenceEquals(other, null) && this.name == other.name && this.version == other.version && this.package == other.package; } public override int GetHashCode() { int result = this.name.GetHashCode(); if (this.version != null) { result ^= this.version.GetHashCode(); } if (this.package != null) { result ^= this.package.GetHashCode(); } return result; } public override string ToString() { StringBuilder result = new StringBuilder(this.name); if (this.version != null) { result.Append(versionString); result.Append(this.version.ToString()); } if (this.package != null) { result.Append(packageString); result.Append(this.package); } return result.ToString(); } [DataMember(EmitDefaultValue = false, Name = "name")] internal string SerializedName { get { return this.name; } set { this.name = value; } } // Version is [Serializable], which isn't supported in PT, so need to convert it to string [DataMember(EmitDefaultValue = false, Name = "version")] internal string SerializedVersion { get { return (this.version == null) ? null : this.version.ToString(); } set { if (string.IsNullOrEmpty(value)) { this.version = null; } else { try { this.version = Version.Parse(value); } catch (ArgumentException ex) { WrapInSerializationException(ex); } catch (FormatException ex) { WrapInSerializationException(ex); } catch (OverflowException ex) { WrapInSerializationException(ex); } } } } [DataMember(EmitDefaultValue = false, Name = "package")] internal string SerializedPackage { get { return this.package; } set { this.package = value; } } // SerializationException with an InnerException is the pattern that DCS follows when values aren't convertible. static void WrapInSerializationException(Exception exception) { throw FxTrace.Exception.AsError(new SerializationException(exception.Message, exception)); } static string ValidateName(string name, string paramName) { if (name == null) { throw FxTrace.Exception.ArgumentNull(paramName); } if (name.Contains(";")) { throw FxTrace.Exception.Argument(paramName, SR.IdentityNameSemicolon); } if (HasControlCharacter(name)) { throw FxTrace.Exception.Argument(paramName, SR.IdentityControlCharacter); } if (HasLeadingOrTrailingWhitespace(name)) { throw FxTrace.Exception.Argument(paramName, SR.IdentityWhitespace); } return Normalize(name, paramName); } static string ValidatePackage(string package, string paramName) { if (package == null) { return null; } if (HasControlCharacter(package)) { throw FxTrace.Exception.Argument(paramName, SR.IdentityControlCharacter); } if (HasLeadingOrTrailingWhitespace(package)) { throw FxTrace.Exception.Argument(paramName, SR.IdentityWhitespace); } return Normalize(package, paramName); } static bool HasControlCharacter(string value) { for (int i = 0; i < value.Length; i++) { if (char.IsControl(value, i)) { return true; } } return false; } static bool HasLeadingOrTrailingWhitespace(string value) { return value.Length > 0 && (char.IsWhiteSpace(value[0]) || char.IsWhiteSpace(value[value.Length - 1])); } static string Normalize(string value, string paramName, bool throwOnError = true) { try { string result = value.Normalize(NormalizationForm.FormC); for (int i = result.Length - 1; i >= 0; i--) { if (char.GetUnicodeCategory(result, i) == UnicodeCategory.Format) { result = result.Remove(i, 1); } } return result; } catch (ArgumentException ex) { if (throwOnError) { throw FxTrace.Exception.AsError(new ArgumentException(ex.Message, paramName, ex)); } else { return null; } } } struct IdentityParser { const string paramName = "identity"; bool throwOnError; Match match; string name; Version version; string package; public static WorkflowIdentity Parse(string identity, bool throwOnError) { if (HasControlCharacter(identity)) { if (throwOnError) { throw FxTrace.Exception.Argument(paramName, SR.IdentityControlCharacter); } return null; } IdentityParser parser = new IdentityParser(); parser.throwOnError = throwOnError; parser.match = identityString.Match(identity.Trim()); if (parser.match.Success) { return parser.Parse(); } else if (throwOnError) { throw FxTrace.Exception.Argument(paramName, SR.BadWorkflowIdentityFormat); } else { return null; } } WorkflowIdentity Parse() { if (!ExtractName()) { return null; } if (!ExtractVersion()) { return null; } if (!ExtractPackage()) { return null; } Fx.Assert(!this.name.Contains(";"), "Regex should not have matched semi-colon"); Fx.Assert(!HasLeadingOrTrailingWhitespace(this.name), "Whitespace should have been stripped"); Fx.Assert(this.package == null || !HasLeadingOrTrailingWhitespace(this.package), "Whitespace should have been stripped"); WorkflowIdentity result = new WorkflowIdentity(); result.name = this.name; result.version = this.version; result.package = this.package; return result; } bool ExtractName() { Group nameMatch = this.match.Groups["name"]; Fx.Assert(nameMatch.Success, "RegEx requires name, even if it's empty"); this.name = Normalize(nameMatch.Value.TrimEnd(), paramName, this.throwOnError); return this.name != null; } bool ExtractVersion() { Group versionMatch = this.match.Groups["version"]; if (versionMatch.Success) { string versionString = versionMatch.Value; if (throwOnError) { this.version = Version.Parse(versionString); } else { return Version.TryParse(versionString, out this.version); } } return true; } bool ExtractPackage() { Group packageMatch = match.Groups["package"]; if (packageMatch.Success) { this.package = Normalize(packageMatch.Value, paramName, this.throwOnError); return this.package != null; } return true; } } } }