File: Script\Services\ClientProxyGenerator.cs
Project: ndp\fx\src\xsp\system\Extensions\System.Web.Extensions.csproj (System.Web.Extensions)
//------------------------------------------------------------------------------
// <copyright file="ClientProxyGenerator.cs" company="Microsoft">
//     Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
//------------------------------------------------------------------------------
 
namespace System.Web.Script.Services {
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Text;
    using System.Web;
    using System.Web.Script.Serialization;
 
    internal abstract class ClientProxyGenerator {
        private static string DebugXmlComments = @"/// <param name=""succeededCallback"" type=""Function"" optional=""true"" mayBeNull=""true""></param>
/// <param name=""failedCallback"" type=""Function"" optional=""true"" mayBeNull=""true""></param>
/// <param name=""userContext"" optional=""true"" mayBeNull=""true""></param>
";
        private Hashtable _registeredNamespaces = new Hashtable();
        private Hashtable _ensuredObjectParts = new Hashtable();
        protected StringBuilder _builder;
        protected bool _debugMode;
        // comments are the same in the instance methods as they are in the static methods
        // this cache is used when calculating comments for instance methods, then re-used when
        // writing out static methods.
        private Dictionary<string, string> _docCommentCache;
 
        internal string GetClientProxyScript(WebServiceData webServiceData) {
            if (webServiceData.MethodDatas.Count == 0) return null;
            _builder = new StringBuilder();
 
            if (_debugMode) {
                _docCommentCache = new Dictionary<string, string>();
            }
 
            // Constructor
            GenerateConstructor(webServiceData);
 
            // Prototype functions
            GeneratePrototype(webServiceData);
 
            GenerateRegisterClass(webServiceData);
            GenerateStaticInstance(webServiceData);
            GenerateStaticMethods(webServiceData);
 
            // Generate some client proxy to make some types instantiatable on the client
            GenerateClientTypeProxies(webServiceData);
            GenerateEnumTypeProxies(webServiceData.EnumTypes);
            return _builder.ToString();
        }
 
        protected void GenerateRegisterClass(WebServiceData webServiceData) {
            // Generate registerClass: Foo.NS.WebService.registerClass('Foo.NS.WebService', Sys.Net.WebServiceProxy);
            string typeName = GetProxyTypeName(webServiceData);
            _builder.Append(typeName).Append(".registerClass('").Append(typeName).Append("',Sys.Net.WebServiceProxy);\r\n");
        }
 
        protected virtual void GenerateConstructor(WebServiceData webServiceData) {
            GenerateTypeDeclaration(webServiceData, false);
            _builder.Append("function() {\r\n");
            _builder.Append(GetProxyTypeName(webServiceData)).Append(".initializeBase(this);\r\n");
            GenerateFields();
            _builder.Append("}\r\n");
        }
 
        protected virtual void GeneratePrototype(WebServiceData webServiceData) {
            GenerateTypeDeclaration(webServiceData, true);
            _builder.Append("{\r\n");
            // private method to return the path to be used , returns _path from current instance if set, otherwise returns _path from static instance.
            _builder.Append("_get_path:function() {\r\n var p = this.get_path();\r\n if (p) return p;\r\n else return ");
            _builder.Append(GetProxyTypeName(webServiceData)).Append("._staticInstance.get_path();},\r\n");
            bool first = true;
            foreach (WebServiceMethodData methodData in webServiceData.MethodDatas) {
                if (!first) {
                    _builder.Append(",\r\n");
                }
                first = false;
                GenerateWebMethodProxy(methodData);
            }
            _builder.Append("}\r\n");
        }
 
        protected virtual void GenerateTypeDeclaration(WebServiceData webServiceData, bool genClass) {
            AppendClientTypeDeclaration(webServiceData.TypeData.TypeNamespace, webServiceData.TypeData.TypeName, genClass, true);
        }
 
        protected void GenerateFields() {
            _builder.Append("this._timeout = 0;\r\n");
            _builder.Append("this._userContext = null;\r\n");
            _builder.Append("this._succeeded = null;\r\n");
            _builder.Append("this._failed = null;\r\n");
        }
 
        protected virtual void GenerateMethods() {
        }
 
        protected void GenerateStaticMethods(WebServiceData webServiceData) {
            string className = GetProxyTypeName(webServiceData);
            // Now generate static methods NS.Service.MyMethod = function()
            foreach (WebServiceMethodData methodData in webServiceData.MethodDatas) {
                string methodName = methodData.MethodName;
                _builder.Append(className).Append('.').Append(methodName).Append("= function(");
                StringBuilder argBuilder = new StringBuilder();
                bool first = true;
                foreach (WebServiceParameterData paramData in methodData.ParameterDatas) {
                    if (!first) argBuilder.Append(',');
                    else first = false;
                    argBuilder.Append(paramData.ParameterName);
                }
                if (!first) argBuilder.Append(',');
                argBuilder.Append("onSuccess,onFailed,userContext");
 
                _builder.Append(argBuilder.ToString()).Append(") {");
 
                if (_debugMode) {
                    // doc comments should have been computed already
                    _builder.Append("\r\n");
                    _builder.Append(_docCommentCache[methodName]);
                }
 
                _builder.Append(className).Append("._staticInstance.").Append(methodName).Append('(');
                _builder.Append(argBuilder.ToString()).Append("); }\r\n");
            }
        }
 
        protected abstract string GetProxyPath();
 
        protected virtual string GetJsonpCallbackParameterName() {
            return null;
        }
 
        protected virtual bool GetSupportsJsonp() {
            return false;
        }
 
        protected void GenerateStaticInstance(WebServiceData data) {
            string typeName = GetProxyTypeName(data);
            _builder.Append(typeName).Append("._staticInstance = new ").Append(typeName).Append("();\r\n");
 
            // Generate the static properties
            if (_debugMode) {
                _builder.Append(typeName).Append(".set_path = function(value) {\r\n");
                _builder.Append(typeName).Append("._staticInstance.set_path(value); }\r\n");
                _builder.Append(typeName).Append(".get_path = function() { \r\n/// <value type=\"String\" mayBeNull=\"true\">The service url.</value>\r\nreturn ");
                _builder.Append(typeName).Append("._staticInstance.get_path();}\r\n");
                
                _builder.Append(typeName).Append(".set_timeout = function(value) {\r\n");
                _builder.Append(typeName).Append("._staticInstance.set_timeout(value); }\r\n");
                _builder.Append(typeName).Append(".get_timeout = function() { \r\n/// <value type=\"Number\">The service timeout.</value>\r\nreturn ");
                _builder.Append(typeName).Append("._staticInstance.get_timeout(); }\r\n");
                
                _builder.Append(typeName).Append(".set_defaultUserContext = function(value) { \r\n");
                _builder.Append(typeName).Append("._staticInstance.set_defaultUserContext(value); }\r\n");
                _builder.Append(typeName).Append(".get_defaultUserContext = function() { \r\n/// <value mayBeNull=\"true\">The service default user context.</value>\r\nreturn ");
                _builder.Append(typeName).Append("._staticInstance.get_defaultUserContext(); }\r\n");
                
                _builder.Append(typeName).Append(".set_defaultSucceededCallback = function(value) { \r\n ");
                _builder.Append(typeName).Append("._staticInstance.set_defaultSucceededCallback(value); }\r\n");
                _builder.Append(typeName).Append(".get_defaultSucceededCallback = function() { \r\n/// <value type=\"Function\" mayBeNull=\"true\">The service default succeeded callback.</value>\r\nreturn ");
                _builder.Append(typeName).Append("._staticInstance.get_defaultSucceededCallback(); }\r\n");
                
                _builder.Append(typeName).Append(".set_defaultFailedCallback = function(value) { \r\n");
                _builder.Append(typeName).Append("._staticInstance.set_defaultFailedCallback(value); }\r\n");
                _builder.Append(typeName).Append(".get_defaultFailedCallback = function() { \r\n/// <value type=\"Function\" mayBeNull=\"true\">The service default failed callback.</value>\r\nreturn ");
                _builder.Append(typeName).Append("._staticInstance.get_defaultFailedCallback(); }\r\n");
 
                _builder.Append(typeName).Append(".set_enableJsonp = function(value) { ");
                _builder.Append(typeName).Append("._staticInstance.set_enableJsonp(value); }\r\n");
                _builder.Append(typeName).Append(".get_enableJsonp = function() { \r\n/// <value type=\"Boolean\">Specifies whether the service supports JSONP for cross domain calling.</value>\r\nreturn ");
                _builder.Append(typeName).Append("._staticInstance.get_enableJsonp(); }\r\n");
 
                _builder.Append(typeName).Append(".set_jsonpCallbackParameter = function(value) { ");
                _builder.Append(typeName).Append("._staticInstance.set_jsonpCallbackParameter(value); }\r\n");
                _builder.Append(typeName).Append(".get_jsonpCallbackParameter = function() { \r\n/// <value type=\"String\">Specifies the parameter name that contains the callback function name for a JSONP request.</value>\r\nreturn ");
                _builder.Append(typeName).Append("._staticInstance.get_jsonpCallbackParameter(); }\r\n");
            }
            else {
                _builder.Append(typeName).Append(".set_path = function(value) { ");
                _builder.Append(typeName).Append("._staticInstance.set_path(value); }\r\n");
                _builder.Append(typeName).Append(".get_path = function() { return ");
                _builder.Append(typeName).Append("._staticInstance.get_path(); }\r\n");
 
                _builder.Append(typeName).Append(".set_timeout = function(value) { ");
                _builder.Append(typeName).Append("._staticInstance.set_timeout(value); }\r\n");
                _builder.Append(typeName).Append(".get_timeout = function() { return ");
                _builder.Append(typeName).Append("._staticInstance.get_timeout(); }\r\n");
 
                _builder.Append(typeName).Append(".set_defaultUserContext = function(value) { ");
                _builder.Append(typeName).Append("._staticInstance.set_defaultUserContext(value); }\r\n");
                _builder.Append(typeName).Append(".get_defaultUserContext = function() { return ");
                _builder.Append(typeName).Append("._staticInstance.get_defaultUserContext(); }\r\n");
 
                _builder.Append(typeName).Append(".set_defaultSucceededCallback = function(value) { ");
                _builder.Append(typeName).Append("._staticInstance.set_defaultSucceededCallback(value); }\r\n");
                _builder.Append(typeName).Append(".get_defaultSucceededCallback = function() { return ");
                _builder.Append(typeName).Append("._staticInstance.get_defaultSucceededCallback(); }\r\n");
 
                _builder.Append(typeName).Append(".set_defaultFailedCallback = function(value) { ");
                _builder.Append(typeName).Append("._staticInstance.set_defaultFailedCallback(value); }\r\n");
                _builder.Append(typeName).Append(".get_defaultFailedCallback = function() { return ");
                _builder.Append(typeName).Append("._staticInstance.get_defaultFailedCallback(); }\r\n");
 
                _builder.Append(typeName).Append(".set_enableJsonp = function(value) { ");
                _builder.Append(typeName).Append("._staticInstance.set_enableJsonp(value); }\r\n");
                _builder.Append(typeName).Append(".get_enableJsonp = function() { return ");
                _builder.Append(typeName).Append("._staticInstance.get_enableJsonp(); }\r\n");
 
                _builder.Append(typeName).Append(".set_jsonpCallbackParameter = function(value) { ");
                _builder.Append(typeName).Append("._staticInstance.set_jsonpCallbackParameter(value); }\r\n");
                _builder.Append(typeName).Append(".get_jsonpCallbackParameter = function() { return ");
                _builder.Append(typeName).Append("._staticInstance.get_jsonpCallbackParameter(); }\r\n");
            }
 
            // the path has to be the full absolete path if this is a JSONP enabled service. But it is the responsibility
            // of the caller to GetClientProxyScript to pass the full path if appropriate since determining it may be
            // dependant on the specific technology.
            string proxyPath = GetProxyPath();
            if (!String.IsNullOrEmpty(proxyPath) && (proxyPath.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || proxyPath.StartsWith("https://", StringComparison.OrdinalIgnoreCase))) {
                // DevDiv 91322: avoid url encoding the domain portion of an IDN url
                // find the first "/" after the scheme, and only encode after that.
                int domainStart = proxyPath.IndexOf("://", StringComparison.OrdinalIgnoreCase) + "://".Length;
                int domainEnd = proxyPath.IndexOf("/", domainStart, StringComparison.OrdinalIgnoreCase);
                // if no slash after :// was found, it could be a domain only url, http://[some service].com, don't encode any of it
                if (domainEnd != -1) {
                    proxyPath = proxyPath.Substring(0, domainEnd) + HttpUtility.UrlPathEncode(proxyPath.Substring(domainEnd));
                }
            }
            else {
                // it doesn't appear to be an absolute url, at least not an http or https one. All relative paths
                // and other oddities are safely encoded with UrlPathEncode.
                proxyPath = HttpUtility.UrlPathEncode(proxyPath);
            }
            _builder.Append(typeName).Append(".set_path(\"").Append(proxyPath).Append("\");\r\n");
            if (GetSupportsJsonp()) {
                _builder.Append(typeName).Append(".set_enableJsonp(true);\r\n");
                string jsonpParameterName = GetJsonpCallbackParameterName();
                if (!String.IsNullOrEmpty(jsonpParameterName) && !jsonpParameterName.Equals("callback", StringComparison.Ordinal)) {
                    _builder.Append(typeName).Append(".set_jsonpCallbackParameter(").Append(JavaScriptSerializer.SerializeInternal(jsonpParameterName)).Append(");\r\n");
                }
            }
        }
 
        private  void BuildArgsDictionary(WebServiceMethodData methodData, StringBuilder args, StringBuilder argsDict, StringBuilder docComments) {
            argsDict.Append('{');
            foreach (WebServiceParameterData paramData in methodData.ParameterDatas) {
                string name = paramData.ParameterName;
                if (docComments != null) {
                    // looks like: /// <param name="foo" type="ClientType">Namespace.ServerType</param>
                    // client type may not match server type for built in js types like date, number, etc.
                    // client type may be omitted for type Object.
                    docComments.Append("/// <param name=\"").Append(name).Append("\"");
                    Type serverType = ServicesUtilities.UnwrapNullableType(paramData.ParameterType);
                    string clientType = GetClientTypeNamespace(ServicesUtilities.GetClientTypeFromServerType(methodData.Owner, serverType));
                    if (!String.IsNullOrEmpty(clientType)) {
                        docComments.Append(" type=\"").Append(clientType).Append("\"");
                    }
                    docComments.Append(">").Append(serverType.FullName).Append("</param>\r\n");
                }
                if (args.Length > 0) {
                    args.Append(',');
                    argsDict.Append(',');
                }
                args.Append(name);
                argsDict.Append(name).Append(':').Append(name);
            }
            if (docComments != null) {
                // append the built-in comments that all methods have (success, failed, usercontext parameters)
                docComments.Append(DebugXmlComments);
            }
            argsDict.Append("}");
            if (args.Length > 0) {
                args.Append(',');
            }
            args.Append("succeededCallback, failedCallback, userContext");
        }
 
        private void GenerateWebMethodProxy(WebServiceMethodData methodData) {
            string methodName = methodData.MethodName;
            string typeName = GetProxyTypeName(methodData.Owner);
            string useGet = methodData.UseGet ? "true" : "false";
 
            _builder.Append(methodName).Append(':');
            // e.g. MyMethod : function(param1, param2, ..., OnSuccess, OnFailure)
            StringBuilder args = new StringBuilder();
            StringBuilder argsDict = new StringBuilder();
            StringBuilder docComments = null;
            string docCommentsString = null;
            if (_debugMode) {
                docComments = new StringBuilder();
            }
            BuildArgsDictionary(methodData, args, argsDict, docComments);
 
            if (_debugMode) {
                // Remember the doc comments for the static instance case
                docCommentsString = docComments.ToString();
                _docCommentCache[methodName] = docCommentsString;
            }
 
            // Method calls look like this.invoke(FooNS.Sub.Method.get_path(), 'MethodName', true[useGet], {'arg1':'val1', 'arg2':'val2' }, onComplete, onError, userContext, 'FooNS.Sub.Method')
            _builder.Append("function(").Append(args.ToString()).Append(") {\r\n");
            if (_debugMode) {
                // docCommentsString always end in \r\n
                _builder.Append(docCommentsString);
            }
            _builder.Append("return this._invoke(this._get_path(), ");
            _builder.Append("'").Append(methodName).Append("',");
            _builder.Append(useGet).Append(',');
            _builder.Append(argsDict.ToString()).Append(",succeededCallback,failedCallback,userContext); }");
        }
 
        /* e.g
          var Qqq = function() { this.__type = "Qqq"; }
         */
        private void GenerateClientTypeProxies(WebServiceData data) {
            bool first = true;
            foreach (WebServiceTypeData t in data.ClientTypes) {
                if (first) {
                    _builder.Append("var gtc = Sys.Net.WebServiceProxy._generateTypedConstructor;\r\n");
                    first = false;
                }
 
                string typeID = data.GetTypeStringRepresentation(t);
                string typeNameWithClientNamespace = GetClientTypeNamespace(t.TypeName);
                string typeName = ServicesUtilities.GetClientTypeName(typeNameWithClientNamespace);
                string clientTypeNamespace = GetClientTypeNamespace(t.TypeNamespace);
 
                EnsureNamespace(t.TypeNamespace);
                EnsureObjectGraph(clientTypeNamespace, typeName);
                _builder.Append("if (typeof(").Append(typeName).Append(") === 'undefined') {\r\n");
                AppendClientTypeDeclaration(clientTypeNamespace, typeNameWithClientNamespace, false, false);
                // Need to use the _type id, which isn't necessarly the real name
                _builder.Append("gtc(\"");
                _builder.Append(typeID);
                _builder.Append("\");\r\n");
                _builder.Append(typeName).Append(".registerClass('").Append(typeName).Append("');\r\n}\r\n");
            }
        }
 
        // Create client stubs for all the enums
        private void GenerateEnumTypeProxies(IEnumerable<WebServiceEnumData> enumTypes) {
            foreach (WebServiceEnumData t in enumTypes) {
                EnsureNamespace(t.TypeNamespace);
                string typeNameWithClientNamespace = GetClientTypeNamespace(t.TypeName);
                string typeName = ServicesUtilities.GetClientTypeName(typeNameWithClientNamespace);
                string[] enumNames = t.Names;
                long[] enumValues = t.Values;
                Debug.Assert(enumNames.Length == enumValues.Length);
                EnsureObjectGraph(GetClientTypeNamespace(t.TypeNamespace), typeName);
                _builder.Append("if (typeof(").Append(typeName).Append(") === 'undefined') {\r\n");
                if (typeName.IndexOf('.') == -1) {
                    _builder.Append("var ");
                }
                _builder.Append(typeName).Append(" = function() { throw Error.invalidOperation(); }\r\n");
                _builder.Append(typeName).Append(".prototype = {");
                for (int i = 0; i < enumNames.Length; i++) {
                    if (i > 0) _builder.Append(',');
                    _builder.Append(enumNames[i]);
                    _builder.Append(": ");
                    if (t.IsULong) {
                        _builder.Append((ulong)enumValues[i]);
                    }
                    else {
                        _builder.Append(enumValues[i]);
                    }
                }
                _builder.Append("}\r\n");
                _builder.Append(typeName).Append(".registerEnum('").Append(typeName).Append('\'');
                _builder.Append(", true);\r\n}\r\n");
            }
        }
 
        protected virtual string GetClientTypeNamespace(string ns) {
            return ns;
        }
 
        private void AppendClientTypeDeclaration(string ns, string typeName, bool genClass, bool ensureNS) {
            // Register the namespace if any
            // e.g. registerNamespace('MyNS.MySubNS');
            string name = GetClientTypeNamespace(ServicesUtilities.GetClientTypeName(typeName));
            if (!String.IsNullOrEmpty(ns)) {
                if (ensureNS) EnsureNamespace(ns);
            }
            else if (!genClass) {
                // If there is no namespace, we need a var to declare the variable
                if (!name.Contains(".")) {
                    // if name contains '.', an object graph was already ensured and we dont need 'var'.
                    _builder.Append("var ");
                }
            }
            _builder.Append(name);
            if (genClass) {
                _builder.Append(".prototype");
            }
            _builder.Append('=');
            _ensuredObjectParts[name] = null;
        }
 
        // Normally returns MyNS.MySubNS.MyWebService OR var MyWebService, PageMethods will return PageMethods
        protected virtual string GetProxyTypeName(WebServiceData data) {
            return ServicesUtilities.GetClientTypeName(data.TypeData.TypeName);
        }
 
        private void EnsureNamespace(string ns) {
            //Give derived proxy generator a chance to transform namespace ( used by WCF)
            ns = GetClientTypeNamespace(ns);
 
            if (String.IsNullOrEmpty(ns)) return;
         
            // Don't register a given namespace more than once
            if (!_registeredNamespaces.Contains(ns)) {
                _builder.Append("Type.registerNamespace('").Append(ns).Append("');\r\n");
                _registeredNamespaces[ns] = null;
            }
        }
 
        private void EnsureObjectGraph(string namespacePart, string typeName) {
            // When a type name includes dots, such as 'MyNamespace.MyClass.MyNestedClass',
            // this method writes code that ensures all the parts leading up to the actual class name
            // are either already namespaces or are at least Objects.
            // namespacePart is here so we dont unnecessarily ensure the first part that contains the
            // namespace is checked for. For example, if we have NS1.NS2.NS3.TYPE, the check for
            // _registeredNamespaces will find NS1.NS2.NS3 but not NS1 and NS1.NS2, so we'd insert
            // checks that NS1 and NS1.NS2 are objects, unnecessarily.
            int startFrom = 0;
            bool first = true;
            if (!String.IsNullOrEmpty(namespacePart)) {
                int nsIndex = typeName.IndexOf(namespacePart + ".", StringComparison.Ordinal);
                // in wcf services, the typeName starts with the namespace,
                // in asmx services, it doesnt.
                if (nsIndex > -1) {
                    startFrom = nsIndex + namespacePart.Length + 1;
                    first = false;
                }
            }
            int dotIndex = typeName.IndexOf('.', startFrom);
            while (dotIndex > -1) {
                string fullPath = typeName.Substring(0, dotIndex);
                if (!_registeredNamespaces.Contains(fullPath) && !_ensuredObjectParts.Contains(fullPath)) {
                    _ensuredObjectParts[fullPath] = null;
                    _builder.Append("if (typeof(" + fullPath + ") === \"undefined\") {\r\n   ");
                    if (first) {
                        // var foo = {};
                        _builder.Append("var ");
                        first = false;
                    }
                    // foo.bar = {};
                    _builder.Append(fullPath + " = {};\r\n}\r\n");
                }
                dotIndex = typeName.IndexOf('.', dotIndex + 1);
            }
        }
    }
}