File: System\Activities\WorkflowIdentity.cs
Project: ndp\cdf\src\NetFx40\System.Activities\System.Activities.csproj (System.Activities)
//-----------------------------------------------------------------------------
// 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<WorkflowIdentity>
    {
        static Regex identityString = new Regex(
            @"^(?<name>[^;]*)
               (; (\s* Version \s* = \s* (?<version>[^;]*))? )?
               (; (\s* Package \s* = \s* (?<package>.*))? )?
              $", 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;
            }
        }
    }
}