using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Data; using System.Data.Odbc; using System.Data.OleDb; using System.Data.SqlClient; using System.Linq; using System.Net; using System.Text; using System.Text.RegularExpressions; using System.Xml.Linq; using Neo.Afx.ComponentModel; using SystemData = System.Data; using System.IO; using Neo.Afx.Common; namespace Neo.Afx.Services.Integration { // ReSharper disable LoopCanBeConvertedToQuery // // DO NOT use LINQ statements as the compiler generated code for these may contain // static fields which are not allowed by SQL 2005 (unless they are read-only) // /// /// A data store for 's. /// public abstract partial class IntegrationStore { readonly static IEnumerable EmptyResult = new XElement[0]; const char ParameterPrefix = '@'; const string ParameterMatchPattern = @"@{1,}\w*"; // matches @ or @anyword or @@anyword or @@@anyword, etc. const string EntityTag = "e"; const string EntityCollectionTag = "d"; const string CachePrefix = "IntegrationStore_"; const string CacheKeyIsCacheEnabled = CachePrefix + "IsCacheEnabled"; const string CacheKeyDataSources = CachePrefix + "DataSources"; const int CacheTimeout = 1440; #region Initialization /// /// Initializes a new instance of the class. /// /// The connection string. protected IntegrationStore(string connectionString) { ConnectionString = connectionString; } #endregion #region ConnectionString /// /// Gets the connection string. /// public string ConnectionString { get; private set; } #endregion #region DataSources /// /// Gets the dictionary of . /// Dictionary DataSources { get; set; } #endregion #region IsCacheEnabled /// /// Gets or sets a value indicating whether caching is enabled. /// public bool IsCacheEnabled { get; private set; } #endregion #region CacheContext /// /// Gets or sets the . /// public ICacheContext CacheContext { get; set; } #endregion #region GetData /// /// Gets the data from the named data source. /// /// The must be invoked before calling this method. /// /// /// The name of the data source to be used. /// The query string to be used. /// The request query - overrides the database value if specified. /// The data from the named data source. public XElement GetData(string name, string queryString, string requestQuery = null) { EnsureCache(); var ds = GetDataSource(name); if(ds == null) { return ToEntitiesXml(EmptyResult); } if(!string.IsNullOrEmpty(requestQuery)) { ds.RequestQuery = requestQuery; } var cacheKey = CachePrefix + name; var isCacheEnabled = CacheContext != null && IsCacheEnabled && ds.IsCacheEnabled; if(isCacheEnabled) { var cachedEntities = CacheContext.Get(cacheKey); if(cachedEntities != null) { return cachedEntities; } } var resultList = EmptyResult; var queryStringArgs = queryString.ToNameValueCollection(); switch(ds.ConnectionTypeEnum) { case ConnectionTypeEnum.SqlServer: case ConnectionTypeEnum.OleDb: case ConnectionTypeEnum.Odbc: case ConnectionTypeEnum.Oracle: resultList = ExecuteDbRequest(ds, queryStringArgs); break; case ConnectionTypeEnum.Web: resultList = ExecuteWebRequest(ds, queryStringArgs); break; } XElement result; if(ds.EntitySchemaDoc == null) { result = resultList.FirstOrDefault(); } else { var top = queryStringArgs["top"]; int count; Int32.TryParse(top, out count); if(count > 0) { resultList = resultList.Take(count); } result = ToEntitiesXml(resultList); } if(isCacheEnabled) { CacheContext.Set(cacheKey, result, ds.CacheMinutes); } return result; } #endregion ///////////////////////////////////////////////////////////////////////////// // Abstract methods ///////////////////////////////////////////////////////////////////////////// #region Open /// /// Establishes a connection to the data store. /// /// Must be invoked before the first call to any read method. /// /// public abstract void Open(); #endregion #region Read /// /// Gets a dictionary of all from the data store. /// /// must be invoked before the first call to this. /// /// /// A dictionary of all keyed by name. protected abstract Dictionary Read(); /// /// Reads the from the data store. /// /// must be invoked before the first call to this. /// /// /// The name of the data source to be read. /// A if the read was successful; null otherwise. protected abstract DataSource Read(string dataSourceName); #endregion #region ReadCacheEnabled /// /// Reads from the data store whether caching is enabled globally. /// /// must be invoked before the first call to this. /// /// /// true if caching is enabled; false otherwise. protected abstract bool ReadCacheEnabled(); #endregion #region Close /// /// Closes connection to the data store /// /// This should dispose of managed resources e.g. connections that were created when was invoked. /// This method is automatically called when this object is disposed. /// /// public abstract void Close(); #endregion ///////////////////////////////////////////////////////////////////////////// // Helper methods for caching ///////////////////////////////////////////////////////////////////////////// #region ClearCache /// /// Clears the cache. /// public void ClearCache() { if(CacheContext != null) { CacheContext.Clear(); } } /// /// Removes the given data source from the cache. /// /// The name of the data source to be removed. public void ClearCache(string dataSourceName) { if(CacheContext != null) { CacheContext.Remove(CachePrefix + dataSourceName); } } #endregion #region EnsureCache void EnsureCache() { IsCacheEnabled = false; DataSources = null; //////if(CacheContext != null) //////{ ////// var result = CacheContext.Get(CacheKeyIsCacheEnabled); ////// if(result == null) ////// { ////// IsCacheEnabled = ReadCacheEnabled(); ////// CacheContext.Set(CacheKeyIsCacheEnabled, IsCacheEnabled, CacheTimeout); ////// } ////// else ////// { ////// IsCacheEnabled = (bool)result; ////// } ////// if(IsCacheEnabled) ////// { ////// result = CacheContext.Get(CacheKeyDataSources); ////// if(result == null) ////// { ////// DataSources = Read(); ////// CacheContext.Set(CacheKeyDataSources, DataSources, CacheTimeout); ////// } ////// else ////// { ////// DataSources = (Dictionary)result; ////// } ////// } //////} } #endregion ///////////////////////////////////////////////////////////////////////////// // Helper methods for creating a data source ///////////////////////////////////////////////////////////////////////////// #region GetDataSource DataSource GetDataSource(string dataSourceName) { return IsCacheEnabled ? DataSources[dataSourceName] : Read(dataSourceName); } #endregion #region CreateDataSource /// /// Creates the using the given function to retrieve field values. /// /// The function used to retrieve field values. /// A or null if is null. protected static DataSource CreateDataSource(Func getValue) { if(getValue != null) { var dataSource = new DataSource { DataSourceID = Int32.Parse(getValue(DataSourceField.DataSourceID)), DataSourceName = getValue(DataSourceField.DataSourceName), ConnectionTypeEnum = (ConnectionTypeEnum)Enum.Parse(typeof(ConnectionTypeEnum), getValue(DataSourceField.ConnectionType), true), CommandTypeEnum = (CommandTypeEnum)Enum.Parse(typeof(CommandTypeEnum), getValue(DataSourceField.CommandType), true), ConnectionString = getValue(DataSourceField.ConnectionString), RequestQuery = getValue(DataSourceField.RequestQuery), ResponseXsl = getValue(DataSourceField.ResponseXsl), EntitySchemaDoc = getValue(DataSourceField.EntitySchema).ToXElement(), IsCacheEnabled = getValue(DataSourceField.IsCacheEnabled).ToBoolean(), CacheMinutes = Int32.Parse(getValue(DataSourceField.CacheMinutes)), AuthTypeEnum = (AuthTypeEnum)Enum.Parse(typeof(AuthTypeEnum), getValue(DataSourceField.AuthType), true), AuthDomain = getValue(DataSourceField.AuthDomain), AuthUser = getValue(DataSourceField.AuthUser), AuthPassword = getValue(DataSourceField.AuthPassword) }; AppendRequestHeaders(dataSource, getValue(DataSourceField.RequestHeaders)); SetProperties(dataSource); return dataSource; } return null; } #endregion #region AppendRequestHeaders static void AppendRequestHeaders(DataSource dataSource, string requestHeaders) { if(!string.IsNullOrEmpty(requestHeaders)) { var headers = XElement.Parse(requestHeaders); foreach(var header in headers.Elements()) { // ReSharper disable PossibleNullReferenceException dataSource.RequestHeadersCollection.Add(header.Attribute("name").Value, header.Attribute("value").Value); // ReSharper restore PossibleNullReferenceException } } } #endregion #region SetProperties static void SetProperties(DataSource dataSource) { if(dataSource.EntitySchemaDoc == null) { return; } dataSource.EntityName = dataSource.EntitySchemaDoc.ToString("name", string.Empty); dataSource.KeySeparator = dataSource.EntitySchemaDoc.ToString("keySeparator", ","); dataSource.TitleSeparator = dataSource.EntitySchemaDoc.ToString("titleSeparator", " "); foreach(var element in dataSource.EntitySchemaDoc.Elements()) { var name = element.ToString("name", string.Empty); if(string.IsNullOrEmpty(name)) { continue; } var isKey = element.ToBoolean("isKey", false); var isTitle = element.ToBoolean("isTitle", false); var isAttribute = element.ToBoolean("isAttribute", false); var property = new DataSourceProperty { Name = name, IsAttribute = isAttribute }; if(isKey) { dataSource.KeyProperties.Add(property); } if(isTitle) { dataSource.TitleProperties.Add(property); } if(!(isKey || isTitle)) { dataSource.Properties.Add(property); } } } #endregion ///////////////////////////////////////////////////////////////////////////// // Helper methods for fetching data from a data source ///////////////////////////////////////////////////////////////////////////// #region ExecuteWebRequest static IEnumerable ExecuteWebRequest(DataSource ds, NameValueCollection queryString) { var responseText = string.Empty; var webClient = new IntegrationWebClient(); webClient.Headers.Add(ds.RequestHeadersCollection); if(ds.AuthTypeEnum == AuthTypeEnum.NetworkCredential) { webClient.Credentials = new NetworkCredential(ds.AuthUser, ds.AuthPassword, ds.AuthDomain); } var request = GetWebParameters(ds, queryString); switch(ds.CommandTypeEnum) { case CommandTypeEnum.Get: var requestUrl = string.Format("{0}?{1}", ds.ConnectionString, request); responseText = webClient.DownloadString(requestUrl); break; case CommandTypeEnum.Post: responseText = webClient.UploadString(ds.ConnectionString, request); break; } return ParseResponse(ds, responseText); } #endregion #region ExecuteDbRequest static IEnumerable ExecuteDbRequest(DataSource ds, NameValueCollection queryString) { using(var conn = CreateDbConnection(ds.ConnectionTypeEnum, ds.ConnectionString)) { if(conn == null) { return null; } using(var cmd = conn.CreateCommand()) { AddDbParameters(ds, queryString, cmd.Parameters); cmd.CommandType = (ds.CommandTypeEnum == CommandTypeEnum.StoredProcedure || ds.CommandTypeEnum == CommandTypeEnum.StoredProcedureExec) ? SystemData.CommandType.StoredProcedure : SystemData.CommandType.Text; cmd.CommandText = (ds.CommandTypeEnum == CommandTypeEnum.StoredProcedure || ds.CommandTypeEnum == CommandTypeEnum.StoredProcedureExec) ? ds.RequestQuery.Substring(0, ds.RequestQuery.IndexOf("@")).Replace("@", "") : ds.RequestQuery; cmd.Connection.Open(); var table = new DataTable(); if(ds.CommandTypeEnum != CommandTypeEnum.StoredProcedureExec) { using(var reader = cmd.ExecuteReader(CommandBehavior.SchemaOnly & CommandBehavior.KeyInfo)) { table.Load(reader, LoadOption.OverwriteChanges); } } else { cmd.ExecuteNonQuery(); } return ParseResponse(ds, table); } } } #endregion #region CreateDbConnection static IDbConnection CreateDbConnection(ConnectionTypeEnum connectionType, string connectionString) { switch(connectionType) { case ConnectionTypeEnum.SqlServer: return new SqlConnection(connectionString); case ConnectionTypeEnum.OleDb: return new OleDbConnection(connectionString); case ConnectionTypeEnum.Odbc: return new OdbcConnection(connectionString); case ConnectionTypeEnum.Oracle: break; } return null; } #endregion #region AddDbParameters static void AddDbParameters(DataSource ds, NameValueCollection queryString, IList parameters) { var paramMatches = new List(); var matches = Regex.Matches(ds.RequestQuery, ParameterMatchPattern); foreach(Match match in matches) { var paramName = match.Value; if(!paramMatches.Contains(paramName)) { paramMatches.Add(paramName); var value = GetDbParameterValue(queryString, paramName); switch(ds.ConnectionTypeEnum) { case ConnectionTypeEnum.SqlServer: parameters.Add(new SqlParameter(paramName, value)); break; case ConnectionTypeEnum.OleDb: parameters.Add(new OleDbParameter(paramName, value)); break; case ConnectionTypeEnum.Odbc: parameters.Add(new OdbcParameter(paramName, value)); break; case ConnectionTypeEnum.Oracle: break; } } } } #endregion #region GetDbParameterValue static string GetDbParameterValue(NameValueCollection queryString, string paramName) { var key = paramName.Remove(0, 1); if(key.Length > 0 && key[0] == ParameterPrefix) { // Do not replace parameters with a @@ prefix just remove the extra @ return key; } return queryString[key] ?? string.Empty; } #endregion #region GetWebParameters static string GetWebParameters(DataSource ds, NameValueCollection queryString) { return Regex.Replace(ds.RequestQuery, ParameterMatchPattern, m => GetDbParameterValue(queryString, m.Value)); } #endregion #region ParseResponse static IEnumerable ParseResponse(DataSource ds, string responseText) { if(string.IsNullOrEmpty(responseText)) { return null; } var response = string.IsNullOrEmpty(ds.ResponseXsl) ? XDocument.Parse(responseText) : responseText.Transform(ds.ResponseXsl); var result = new List(); if(ds.EntitySchemaDoc == null) { // Don't convert, just return the XML result.Add(response.Root); } else if(response.Root != null && !response.Root.IsEmpty) { var entities = response.Root.HasElements ? response.Children(ds.EntityName) : XDocument.Parse(response.Root.Value).Children(ds.EntityName); foreach(var entity in entities) { var e = entity; var xml = ToEntityXml(ds, p => p.IsAttribute ? e.ToString(p.Name, string.Empty) : e.Child(p.Name).ToString(string.Empty)); result.Add(xml); } } return result; } static IEnumerable ParseResponse(DataSource ds, DataTable dt) { var result = new List(); foreach(DataRow dr in dt.Rows) { var row = dr; result.Add(ToEntityXml(ds, p => row[p.Name].ToStringSafe())); } return result; } #endregion #region ToEntitiesXml static XElement ToEntitiesXml(IEnumerable entities) { var result = new XElement(EntityCollectionTag); if(entities != null) { foreach(var entity in entities) { result.Add(entity); } } return result; } #endregion #region ToEntityXml static XElement ToEntityXml(DataSource dataSource, Func getValue) { var result = new XElement(EntityTag, new XElement("key", JoinNames(dataSource.KeyProperties, dataSource.KeySeparator, getValue)), new XElement("title", JoinNames(dataSource.TitleProperties, dataSource.TitleSeparator, getValue)) ); if(dataSource.Properties.Count > 0) { var properties = new XElement("properties"); foreach(var property in dataSource.Properties) { properties.Add(new XElement(property.Name.ToLower(), getValue(property))); } result.Add(properties); } return result; } #endregion #region JoinNames static string JoinNames(IEnumerable properties, string separator, Func getValue) { var result = new StringBuilder(); foreach(var property in properties) { var value = getValue(property); if(!string.IsNullOrEmpty(value)) { if(result.Length > 0) { result.Append(separator); } result.Append(value); } } return result.ToString(); } #endregion } // ReSharper restore LoopCanBeConvertedToQuery }