File: Rules\RuleAttributes.cs
Project: ndp\cdf\src\WF\Activities\System.Workflow.Activities.csproj (System.Workflow.Activities)
// ---------------------------------------------------------------------------
// Copyright (C) 2005 Microsoft Corporation All Rights Reserved
// ---------------------------------------------------------------------------
 
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Globalization;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Compiler;
using System.Reflection;
using System.Workflow.Activities.Common;
 
 
namespace System.Workflow.Activities.Rules
{
    public enum RuleAttributeTarget
    {
        Parameter,
        This
    }
 
    public abstract class RuleAttribute : Attribute
    {
        internal abstract bool Validate(RuleValidation validation, MemberInfo member, Type contextType, ParameterInfo[] parameters);
        internal abstract void Analyze(RuleAnalysis analysis, MemberInfo member, CodeExpression targetExpression, RulePathQualifier targetQualifier, CodeExpressionCollection argumentExpressions, ParameterInfo[] parameters, List<CodeExpression> attributedExpressions);
    }
 
    public abstract class RuleReadWriteAttribute : RuleAttribute
    {
        private RuleAttributeTarget attributeTarget;
        private string attributePath;
 
        protected RuleReadWriteAttribute(string path, RuleAttributeTarget target)
        {
            this.attributeTarget = target;
            this.attributePath = path;
        }
 
        public string Path
        {
            get { return attributePath; }
        }
 
        public RuleAttributeTarget Target
        {
            get { return attributeTarget; }
        }
 
        internal override bool Validate(RuleValidation validation, MemberInfo member, Type contextType, ParameterInfo[] parameters)
        {
            ValidationError error = null;
            string message = null;
 
            if (string.IsNullOrEmpty(attributePath))
            {
                // It is allowed to pass null or the empty string to [RuleRead] or [RuleWrite].  This
                // is how you indicate that a method or property has no dependencies or side effects.
                return true;
            }
 
            bool valid = true;
 
            string[] parts = attributePath.Split('/');
 
            // Check the first part.
 
            string firstPart = parts[0];
            int startOfRelativePortion = 0;
            if (attributeTarget == RuleAttributeTarget.This)
            {
                // When target is "This", the path is allowed to start with the token "this".  It is
                // then skipped for the rest of the validation, and the contextType remains what it
                // was when passed in.
                if (firstPart == "this")
                    ++startOfRelativePortion;
            }
            else
            {
                // When target is "Parameter", the path must start with the name of a parameter.
                bool found = false;
                for (int p = 0; p < parameters.Length; ++p)
                {
                    ParameterInfo param = parameters[p];
                    if (param.Name == firstPart)
                    {
                        found = true;
 
                        // The context type is the parameter type.
                        contextType = param.ParameterType;
                        break;
                    }
                }
 
                if (!found)
                {
                    message = string.Format(CultureInfo.CurrentCulture, Messages.InvalidRuleAttributeParameter, firstPart, member.Name);
                    error = new ValidationError(message, ErrorNumbers.Error_InvalidRuleAttributeParameter);
                    error.UserData[RuleUserDataKeys.ErrorObject] = this;
                    validation.AddError(error);
                    return false;
                }
 
                ++startOfRelativePortion;
            }
 
            int numParts = parts.Length;
 
            // Check the last part.  The last part is allowed to be empty, or "*".
 
            string lastPart = parts[numParts - 1];
            if (string.IsNullOrEmpty(lastPart) || lastPart == "*")
                numParts -= 1;
 
            // Check the rest of the parts.
 
            Type currentType = contextType;
            for (int i = startOfRelativePortion; i < numParts; ++i)
            {
                // Can't have embedded "*" wildcards.
                if (parts[i] == "*")
                {
                    // The "*" occurred in the middle of the path, which is a no-no.
                    error = new ValidationError(Messages.InvalidWildCardInPathQualifier, ErrorNumbers.Error_InvalidWildCardInPathQualifier);
                    error.UserData[RuleUserDataKeys.ErrorObject] = this;
                    validation.AddError(error);
                    valid = false;
                    break;
                }
 
                // Skip array types.
                while (currentType.IsArray)
                    currentType = currentType.GetElementType();
 
                // Make sure the member exists in the current type.
                BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy;
                if (validation.AllowInternalMembers(currentType))
                    bindingFlags |= BindingFlags.NonPublic;
 
                FieldInfo field = currentType.GetField(parts[i], bindingFlags);
                if (field != null)
                {
                    currentType = field.FieldType;
                }
                else
                {
                    PropertyInfo property = currentType.GetProperty(parts[i], bindingFlags);
                    if (property != null)
                    {
                        currentType = property.PropertyType;
                    }
                    else
                    {
                        message = string.Format(CultureInfo.CurrentCulture, Messages.UpdateUnknownFieldOrProperty, parts[i]);
                        error = new ValidationError(message, ErrorNumbers.Error_UnknownFieldOrProperty);
                        error.UserData[RuleUserDataKeys.ErrorObject] = this;
                        validation.AddError(error);
                        valid = false;
                        break;
                    }
                }
            }
 
            return valid;
        }
 
        internal void AnalyzeReadWrite(RuleAnalysis analysis, CodeExpression targetExpression, RulePathQualifier targetQualifier, CodeExpressionCollection argumentExpressions, ParameterInfo[] parameters, List<CodeExpression> attributedExpressions)
        {
            if (string.IsNullOrEmpty(attributePath))
            {
                // If the suffix is null or empty, this means the RuleAttributeTarget has no dependencies.
                if (attributeTarget == RuleAttributeTarget.This)
                {
                    // The target object has no dependencies.
                    attributedExpressions.Add(targetExpression);
                }
                else if (attributeTarget == RuleAttributeTarget.Parameter)
                {
                    // ALL arguments have no dependencies.
                    for (int i = 0; i < argumentExpressions.Count; ++i)
                        attributedExpressions.Add(argumentExpressions[i]);
                }
            }
            else
            {
                string suffix = attributePath;
 
                bool isRead = !analysis.ForWrites;
                bool isWrite = analysis.ForWrites;
 
                if (attributeTarget == RuleAttributeTarget.This)
                {
                    // Target is "This", so perform the analysis on the target expression.
 
                    // Remove the optional "this/" token if present.
                    string optionalPrefix = "this/";
                    if (suffix.StartsWith(optionalPrefix, StringComparison.Ordinal))
                        suffix = suffix.Substring(optionalPrefix.Length);
 
                    RuleExpressionWalker.AnalyzeUsage(analysis, targetExpression, isRead, isWrite, new RulePathQualifier(suffix, targetQualifier));
                    attributedExpressions.Add(targetExpression);
                }
                else if (attributeTarget == RuleAttributeTarget.Parameter)
                {
                    string paramName = null;
 
                    int firstSlash = suffix.IndexOf('/');
                    if (firstSlash >= 0)
                    {
                        paramName = suffix.Substring(0, firstSlash);
                        suffix = suffix.Substring(firstSlash + 1);
                    }
                    else
                    {
                        paramName = suffix;
                        suffix = null;
                    }
 
                    // Find the ParameterInfo that corresponds to this attribute path.
                    ParameterInfo param = Array.Find<ParameterInfo>(parameters,
                                                                    delegate(ParameterInfo p) { return p.Name == paramName; });
                    if (param != null)
                    {
                        RulePathQualifier qualifier = string.IsNullOrEmpty(suffix) ? null : new RulePathQualifier(suffix, null);
 
                        // 99.9% of the time, the parameter usage attribute only applies to one argument.  However,
                        // if this attribute corresponds to the last parameter, then just assume that all the trailing
                        // arguments correspond.  (In other words, if the caller passed more arguments then there
                        // are parameters, we assume it was a params array.)
                        //
                        // Usually this loop will only execute once.
                        int end = param.Position + 1;
                        if (param.Position == parameters.Length - 1)
                            end = argumentExpressions.Count;
 
                        for (int i = param.Position; i < end; ++i)
                        {
                            CodeExpression argExpr = argumentExpressions[i];
                            RuleExpressionWalker.AnalyzeUsage(analysis, argExpr, isRead, isWrite, qualifier);
                            attributedExpressions.Add(argExpr);
                        }
                    }
                }
            }
        }
    }
 
    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method, AllowMultiple = true)]
    public sealed class RuleReadAttribute : RuleReadWriteAttribute
    {
        public RuleReadAttribute(string path, RuleAttributeTarget target)
            : base(path, target)
        {
        }
 
        public RuleReadAttribute(string path)
            : base(path, RuleAttributeTarget.This)
        {
        }
 
        internal override void Analyze(RuleAnalysis analysis, MemberInfo member, CodeExpression targetExpression, RulePathQualifier targetQualifier, CodeExpressionCollection argumentExpressions, ParameterInfo[] parameters, List<CodeExpression> attributedExpressions)
        {
            // A RuleRead attribute is only applicable if we're analyzing for reads.
            if (analysis.ForWrites)
                return;
 
            base.AnalyzeReadWrite(analysis, targetExpression, targetQualifier, argumentExpressions, parameters, attributedExpressions);
        }
    }
 
    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method, AllowMultiple = true)]
    public sealed class RuleWriteAttribute : RuleReadWriteAttribute
    {
        public RuleWriteAttribute(string path, RuleAttributeTarget target)
            : base(path, target)
        {
        }
 
        public RuleWriteAttribute(string path)
            : base(path, RuleAttributeTarget.This)
        {
        }
 
        internal override void Analyze(RuleAnalysis analysis, MemberInfo member, CodeExpression targetExpression, RulePathQualifier targetQualifier, CodeExpressionCollection argumentExpressions, ParameterInfo[] parameters, List<CodeExpression> attributedExpressions)
        {
            // A RuleWrite attribute is only applicable if we're analyzing for writes.
            if (!analysis.ForWrites)
                return;
 
            base.AnalyzeReadWrite(analysis, targetExpression, targetQualifier, argumentExpressions, parameters, attributedExpressions);
        }
    }
 
 
    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method, AllowMultiple = true)]
    public sealed class RuleInvokeAttribute : RuleAttribute
    {
        private string methodInvoked;
 
        public RuleInvokeAttribute(string methodInvoked)
        {
            this.methodInvoked = methodInvoked;
        }
 
        public string MethodInvoked
        {
            get { return methodInvoked; }
        }
 
        internal override bool Validate(RuleValidation validation, MemberInfo member, Type contextType, ParameterInfo[] parameters)
        {
            Stack<MemberInfo> methodStack = new Stack<MemberInfo>();
            methodStack.Push(member);
 
            bool result = ValidateInvokeAttribute(validation, member, contextType, methodStack);
 
            methodStack.Pop();
 
            return result;
        }
 
        private bool ValidateInvokeAttribute(RuleValidation validation, MemberInfo member, Type contextType, Stack<MemberInfo> methodStack)
        {
            string message;
            ValidationError error;
 
            if (string.IsNullOrEmpty(methodInvoked))
            {
                // Invoked method or property name was null or empty.
                message = string.Format(CultureInfo.CurrentCulture, Messages.AttributeMethodNotFound, member.Name, this.GetType().Name, Messages.NullValue);
                error = new ValidationError(message, ErrorNumbers.Warning_RuleAttributeNoMatch, true);
                error.UserData[RuleUserDataKeys.ErrorObject] = this;
                validation.AddError(error);
                return false;
            }
 
            bool valid = true;
 
            // Go through all the methods and properties on the target context,
            // looking for all the ones that match the name on the attribute.
            MemberInfo[] members = contextType.GetMember(methodInvoked, MemberTypes.Method | MemberTypes.Property, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy);
 
            if (members == null || members.Length == 0)
            {
                // Invoked method or property didn't exist.
                message = string.Format(CultureInfo.CurrentCulture, Messages.AttributeMethodNotFound, member.Name, this.GetType().Name, methodInvoked);
                error = new ValidationError(message, ErrorNumbers.Warning_RuleAttributeNoMatch, true);
                error.UserData[RuleUserDataKeys.ErrorObject] = this;
                validation.AddError(error);
                valid = false;
            }
            else
            {
                for (int i = 0; i < members.Length; ++i)
                {
                    MemberInfo mi = members[i];
                    if (!methodStack.Contains(mi)) // Prevent recursion
                    {
                        methodStack.Push(mi);
 
                        object[] attrs = mi.GetCustomAttributes(typeof(RuleAttribute), true);
                        if (attrs != null && attrs.Length != 0)
                        {
                            foreach (RuleAttribute invokedRuleAttr in attrs)
                            {
                                RuleReadWriteAttribute readWriteAttr = invokedRuleAttr as RuleReadWriteAttribute;
                                if (readWriteAttr != null)
                                {
                                    // This read/write attribute may not specify a target of "Parameter", since
                                    // we can't map from the invoker's parameters to the invokee's parameters.
                                    if (readWriteAttr.Target == RuleAttributeTarget.Parameter)
                                    {
                                        message = string.Format(CultureInfo.CurrentCulture, Messages.InvokeAttrRefersToParameterAttribute, mi.Name);
                                        error = new ValidationError(message, ErrorNumbers.Error_InvokeAttrRefersToParameterAttribute, true);
                                        error.UserData[RuleUserDataKeys.ErrorObject] = this;
                                        validation.AddError(error);
                                        valid = false;
                                    }
                                    else
                                    {
                                        // Validate the read/write attribute normally.
                                        readWriteAttr.Validate(validation, mi, contextType, null);
                                    }
                                }
                                else
                                {
                                    RuleInvokeAttribute invokeAttr = (RuleInvokeAttribute)invokedRuleAttr;
                                    invokeAttr.ValidateInvokeAttribute(validation, mi, contextType, methodStack);
                                }
                            }
                        }
 
                        methodStack.Pop();
                    }
                }
            }
 
            return valid;
        }
 
        internal override void Analyze(RuleAnalysis analysis, MemberInfo member, CodeExpression targetExpression, RulePathQualifier targetQualifier, CodeExpressionCollection argumentExpressions, ParameterInfo[] parameters, List<CodeExpression> attributedExpressions)
        {
            Stack<MemberInfo> methodStack = new Stack<MemberInfo>();
            methodStack.Push(member);
 
            AnalyzeInvokeAttribute(analysis, member.DeclaringType, methodStack, targetExpression, targetQualifier, argumentExpressions, parameters, attributedExpressions);
 
            methodStack.Pop();
        }
 
        private void AnalyzeInvokeAttribute(RuleAnalysis analysis, Type contextType, Stack<MemberInfo> methodStack, CodeExpression targetExpression, RulePathQualifier targetQualifier, CodeExpressionCollection argumentExpressions, ParameterInfo[] parameters, List<CodeExpression> attributedExpressions)
        {
            // Go through all the methods and properties on the target context,
            // looking for all the ones that match the name on the attribute.
            MemberInfo[] members = contextType.GetMember(methodInvoked, MemberTypes.Method | MemberTypes.Property, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy);
 
            for (int m = 0; m < members.Length; ++m)
            {
                MemberInfo mi = members[m];
                if (!methodStack.Contains(mi)) // Prevent recursion
                {
                    methodStack.Push(mi);
 
                    object[] attrs = mi.GetCustomAttributes(typeof(RuleAttribute), true);
                    if (attrs != null && attrs.Length != 0)
                    {
                        RuleAttribute[] ruleAttrs = (RuleAttribute[])attrs;
                        for (int i = 0; i < ruleAttrs.Length; ++i)
                        {
                            RuleAttribute ruleAttr = ruleAttrs[i];
 
                            RuleReadWriteAttribute readWriteAttr = ruleAttr as RuleReadWriteAttribute;
                            if (readWriteAttr != null)
                            {
                                // Just analyze the read/write attribute normally.
                                readWriteAttr.Analyze(analysis, mi, targetExpression, targetQualifier, argumentExpressions, parameters, attributedExpressions);
                            }
                            else
                            {
                                RuleInvokeAttribute invokeAttr = (RuleInvokeAttribute)ruleAttr;
                                invokeAttr.AnalyzeInvokeAttribute(analysis, contextType, methodStack, targetExpression, targetQualifier, argumentExpressions, parameters, attributedExpressions);
                            }
                        }
                    }
 
                    methodStack.Pop();
                }
            }
        }
    }
}