/// <summary> /// Stores a object in the cache /// </summary> /// <param name="data"></param> public void Store(ResponseBucket data, TimeSpan?time = null) { if (time == null) { time = new TimeSpan(0, 2, 0); } //Check if caching is enabled if (!Settings.Current.EnableCache) { return; } if (System.Web.HttpRuntime.Cache[data.Key] == null && data.CreationTime > _cachedStartTime) { if (_apiCacheLogger.IsDebugEnabled) { _apiCacheLogger.DebugFormat("Adding key={0} to Cache", data.Key); } lock (CacheLock) { if (System.Web.HttpRuntime.Cache[data.Key] == null) { System.Web.HttpRuntime.Cache.Add(data.Key, data, null, System.Web.Caching.Cache.NoAbsoluteExpiration, time.Value, System.Web.Caching.CacheItemPriority.Normal, null); } } } }
/// <summary> /// Sends data /// </summary> /// <param name="context"></param> /// <param name="cacheResponse"></param> /// <param name="doCache"></param> public static void Send(this HttpContext context, ResponseBucket cacheResponse, bool doCache) { context.Send(cacheResponse.ContentType, cacheResponse.ResponseData); // Store request in cache if (doCache) { ApiCache.Current.Store(cacheResponse); } }
/// <summary> /// Gets the menu object for the specified database /// </summary> /// <remarks></remarks> /// <returns>A menu object</returns> public static MenuObject GetMenu(string db, string language, string[] nodePath) { if (ExposedDatabases.DatabaseConfigurations[language].ContainsKey(db) == false) { return(null); } var database = ExposedDatabases.DatabaseConfigurations[language][db]; var dbtype = database.Type; string cacheKey = ApiCache.CreateKey($"{db}_{language}_{string.Join("_", nodePath)}"); MenuObject menuObj = null; ResponseBucket cacheResponse = ApiCache.Current.Fetch(cacheKey); if (cacheResponse != null) { return(cacheResponse.Menu); } try { if (dbtype == "PX") { menuObj = MenuObject.Create(GetPxMenu(db, language, string.Join("\\", nodePath))); } else if (dbtype == "CNMM") { string tid = nodePath.Last(); string menu = nodePath.Length > 1 ? nodePath[nodePath.Length - 2]:"START"; if (tid.Contains("'") || menu.Contains("'")) { throw new ArgumentException("Possible SQL injection"); } menuObj = MenuObject.Create(GetCnmmMenu(db, language, tid, menu)); } // Request object to be stored in cache cacheResponse = new ResponseBucket(); cacheResponse.Key = cacheKey; cacheResponse.Menu = menuObj; ApiCache.Current.Store(cacheResponse, new TimeSpan(24, 0, 0)); return(menuObj); } catch (Exception e) { _logger.Error("Error retrieving menu", e); return(null); } }
/// <summary> /// Handles client requests and routes to GET or POST methods accordingly /// </summary> /// <param name="context"></param> public void ProcessRequest(HttpContext context) { #if DEBUG System.Diagnostics.Stopwatch stopWatch = new System.Diagnostics.Stopwatch(); stopWatch.Start(); logTime.Info("Start " + System.Reflection.MethodBase.GetCurrentMethod().Name); #endif try { string cacheKey = ""; // Key to request stored in cache ResponseBucket cacheResponse; // Request stored in cache string postData = ""; // Data part of POST request (contains JSON query) // Negotiate with the request limiter (if enabled) if (_requestLimiter != null) { if (!_requestLimiter.ClientLimitOK(context.Request)) { // Deny request // see 409 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html context.Response.AppendHeader("Retry-After", _requestLimiter.LimiterTimeSpan.ToString()); throw new HttpException(429, "429 - Too many requests in too short timeframe. Please try again later."); } } if (Settings.Current.EnableCORS) { // Enable CORS (Cross-Origin-Resource-Sharing) context.Response.AppendHeader("Access-Control-Allow-Origin", "*"); // Enable Preflight requests if (context.Request.HttpMethod == "OPTIONS") { context.Response.AppendHeader("Access-Control-Allow-Methods", "GET, POST"); context.Response.AppendHeader("Access-Control-Allow-Headers", "Content-Type"); //context.Response.End(); return; } } // Create cache key by combining URL and query strings if (context.Request.HttpMethod == "POST") { using (var stream = new StreamReader(context.Request.InputStream, context.Request.ContentEncoding)) { postData = stream.ReadToEnd(); } } cacheKey = ApiCache.CreateKey(context.Request.Url.AbsoluteUri + postData); // Try to get request from cache cacheResponse = ApiCache.Current.Fetch(cacheKey); if (cacheResponse != null) { if (cacheResponse.Url.Equals(context.Request.Url.AbsoluteUri) && cacheResponse.PostData.Equals(postData)) { // Use cached object context.Send(cacheResponse, false); if (context.Request.HttpMethod == "POST") { _usageLogger.Info(String.Format("url={0}, type=data, caller={3}, cached=true, format={1}, matrix-size={2}", context.Request.RawUrl, cacheResponse.ContentType, "?", context.Request.UserHostAddress)); } else { _usageLogger.Info(String.Format("url={0}, type=metadata, caller={1}, cached=true, format=?, matrix-size=?", context.Request.RawUrl, context.Request.UserHostAddress)); } #if DEBUG stopWatch.Stop(); logTime.InfoFormat(System.Reflection.MethodBase.GetCurrentMethod().Name + " Done from cahce in ms = {0}", stopWatch.ElapsedMilliseconds); #endif return; } } if (_apiCacheLogger.IsDebugEnabled) { _apiCacheLogger.DebugFormat("Key {0} not found in cache, creating response from backend.", cacheKey); } // Request object to be stored in cache cacheResponse = new ResponseBucket(); cacheResponse.Key = cacheKey; cacheResponse.Url = context.Request.Url.AbsoluteUri; cacheResponse.PostData = postData; List <string> routeParts = null; string language = null; string db = null; // Fetch the route data var routeData = context.Items["RouteData"] as RouteData; if (routeData.Values["language"] != null) { language = routeData.Values["language"].ToString(); } if (routeData.Values["path"] != null) { routeParts = routeData.Values["path"].ToString().Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries).ToList(); db = routeParts.First(); routeParts.RemoveAt(0); } // Parse query string var options = new Options(context.Request.QueryString); var queryKeys = context.Request.QueryString.ToString().Split('&'); if (queryKeys.Contains("config")) { var obj = new { maxValues = Settings.Current.MaxValues, maxCells = Settings.Current.FetchCellLimit, maxCalls = Settings.Current.LimiterRequests, timeWindow = Settings.Current.LimiterTimeSpan, CORS = Settings.Current.EnableCORS }; cacheResponse.ContentType = "application/json; charset=" + System.Text.Encoding.UTF8.WebName; cacheResponse.ResponseData = context.Response.ContentEncoding.GetBytes(obj.ToJSON(options.PrettyPrint)); context.Send(cacheResponse, true); _usageLogger.Info(String.Format("url={0}, type=config, caller={1}, cached=false", context.Request.RawUrl, context.Request.UserHostAddress)); #if DEBUG stopWatch.Stop(); logTime.InfoFormat(System.Reflection.MethodBase.GetCurrentMethod().Name + " Done in ms = {0}", stopWatch.ElapsedMilliseconds); #endif return; } if (ExposedDatabases.DatabaseConfigurations.ContainsKey(language) == false) { throw new ArgumentException("The specified language '" + language + "' does not exist"); } else if (db == null) { var databases = new List <MetaDb>(); foreach (var database in ExposedDatabases.DatabaseConfigurations[language].Keys) { databases.Add(new MetaDb { Id = database, Text = ExposedDatabases.DatabaseConfigurations[language][database].Name }); } if (databases.Count > 0) { cacheResponse.ContentType = "application/json; charset=" + System.Text.Encoding.UTF8.WebName; cacheResponse.ResponseData = context.Response.ContentEncoding.GetBytes(databases.ToJSON(options.PrettyPrint)); context.Send(cacheResponse, true); //context.SendJSON(databases.ToJSON(options.PrettyPrint)); } else { throw new ArgumentException("No databases found in the specified language"); } } else // We have a database specified { if (ExposedDatabases.DatabaseConfigurations[language].ContainsKey(db) == false) { // Database does not exist throw new ArgumentException("The specified database '" + db + "' does not exist"); } else { // Try to get a menu item first string[] strNodePath = (routeParts == null || routeParts.Count == 0) ? new string[] { "" } : routeParts.ToArray(); var menu = PCAxisRepository.GetMenu(db, language, strNodePath); if (menu == null) { //TODO Check exception type throw new Exception("Error while connecting to data source"); } else if ((menu.ID.Menu == "") && (menu.ID.Selection == "")) { // Could not find level in tree - Check if it is a short URL with table if (strNodePath.Length == 1 && strNodePath[0] != "") { var possibleNameOrId = strNodePath[0]; PCAxis.Search.SearchStatusType status; var text = string.Format("{0}:{2} OR {1}:{2}", PCAxis.Search.SearchConstants.SEARCH_FIELD_SEARCHID, PCAxis.Search.SearchConstants.SEARCH_FIELD_TABLEID, possibleNameOrId); var results = PCAxis.Search.SearchManager.Current.Search(db, language, text, out status); if (status == Search.SearchStatusType.Successful && results.Count > 0) { if (_logger.IsDebugEnabled) { if (results.Count > 1) { for (int cnter = 1; cnter < results.Count; cnter++) { if (!String.Equals(results[cnter - 1].Table, results[cnter].Table)) { _logger.DebugFormat("Hmmm, not some result have different table names: {0} differs from {1}", results[cnter - 1].Table, results[cnter].Table); } } } } strNodePath = (results[0].Path + "/" + results[0].Table).Split('/'); //Remove first element in array strNodePath = strNodePath.Where((source, index) => index != 0).ToArray(); routeParts = strNodePath.ToList(); // Get the menu again... menu = PCAxisRepository.GetMenu(db, language, strNodePath); } } } //else //{ var currentItem = menu; //.CurrentItem as Item; if (currentItem.MenuType == typeof(PxMenuItem) && currentItem.HasSubItems) { if (!string.IsNullOrEmpty(options.SearchQuery)) { // Find tables using the search function PCAxis.Search.SearchStatusType status; cacheResponse.ContentType = "application/json; charset=" + System.Text.Encoding.UTF8.WebName; cacheResponse.ResponseData = context.Response.ContentEncoding.GetBytes(GetSearchResult(db, language, options.SearchQuery, options.SearchFilter, routeParts, out status).ToJSON(options.PrettyPrint)); if (status == Search.SearchStatusType.Successful) { context.Send(cacheResponse, false); // Send without caching } else { // Trying to search a non-indexed database... context.SendJSONError(Error("Search not activated", false), 400); _usageLogger.Info(String.Format("url={0}, type=error, caller={1}, cached=false, message=Search not activated", context.Request.RawUrl, context.Request.UserHostAddress)); _logger.Warn(String.Format("Search not activated for {0} - {1}", db, language)); #if DEBUG stopWatch.Stop(); logTime.InfoFormat(System.Reflection.MethodBase.GetCurrentMethod().Name + " Done in ms = {0}", stopWatch.ElapsedMilliseconds); #endif return; } } else { // Current item is a list item - return meta data for this list cacheResponse.ContentType = "application/json; charset=" + System.Text.Encoding.UTF8.WebName; cacheResponse.ResponseData = context.Response.ContentEncoding.GetBytes(currentItem.MetaList.ToJSON(options.PrettyPrint)); context.Send(cacheResponse, true); } //context.SendJSON(GetMetaList(context, (PxMenuItem)currentItem).ToJSON(options.PrettyPrint)); } else if (context.Request.HttpMethod == "GET" && currentItem.MenuType == typeof(TableLink)) { // Current item is not a list. Try to return table meta data cacheResponse.ContentType = "application/json; charset=" + System.Text.Encoding.UTF8.WebName; cacheResponse.ResponseData = context.Response.ContentEncoding.GetBytes(GetTableMeta(context, language, db, strNodePath).ToJSON(options.PrettyPrint)); context.Send(cacheResponse, true); // Logs usage _usageLogger.Info(String.Format("url={0}, type=metadata, caller={1}, cached=false, format=json", context.Request.RawUrl, context.Request.UserHostAddress)); } else if (context.Request.HttpMethod == "POST" && currentItem.MenuType == typeof(TableLink)) { // Current item is not a list. Try to return table data. SendTableData(context, language, db, routeParts.ToArray(), cacheResponse); } else { context.SendJSONError(Error("Parameter error", false), 400); // Logs usage _usageLogger.Info(String.Format("url={0}, type=error, caller={1}, cached=false, message=Parameter error", context.Request.RawUrl, context.Request.UserHostAddress)); } //} } } #if DEBUG stopWatch.Stop(); logTime.InfoFormat(System.Reflection.MethodBase.GetCurrentMethod().Name + " Done in ms = {0}", stopWatch.ElapsedMilliseconds); #endif } catch (HttpException ex) { #if DEBUG stopWatch.Stop(); logTime.InfoFormat(System.Reflection.MethodBase.GetCurrentMethod().Name + " Error Done in ms = {0}", stopWatch.ElapsedMilliseconds); #endif if (ex.GetHttpCode() == 429) { context.Response.TrySkipIisCustomErrors = true; context.SendJSONError(Error(ex.Message, false), 429); } else { context.SendJSONError(Error("Parameter error", false), ex.GetHttpCode()); } // Logs usage _usageLogger.Info(String.Format("url={0}, type=error, caller={1}, cached=false", context.Request.RawUrl, context.Request.UserHostAddress), ex); _logger.Warn(ex); } catch (Exception ex) { //if (context.Request.HttpMethod != "OPTIONS") //{ #if DEBUG stopWatch.Stop(); logTime.InfoFormat(System.Reflection.MethodBase.GetCurrentMethod().Name + " Error Done in ms = {0}", stopWatch.ElapsedMilliseconds); #endif try { context.SendJSONError(Error("Parameter error", false), 400); } catch (Exception sendEx) { _logger.Error(String.Format("url={0}, type=error, caller={1}", context.Request.RawUrl, context.Request.UserHostAddress), sendEx); } _usageLogger.Info(String.Format("url={0}, type=error, caller={1}, cached=false ", context.Request.RawUrl, context.Request.UserHostAddress), ex); _logger.Warn(ex); //} } }
/// <summary> /// Serializes and sends table data back to the client /// </summary> /// <param name="context"></param> private void SendTableData(HttpContext context, string language, string db, string[] tablePath, ResponseBucket cacheResponse) { //// TODO: Limit data size? //string data = ""; //using (var stream = new StreamReader(context.Request.InputStream)) //{ // data = stream.ReadToEnd(); //} var tableQuery = JsonHelper.Deserialize <TableQuery>(cacheResponse.PostData) as TableQuery; if (tableQuery.Response == null || tableQuery.Response.Format == null) { tableQuery.Response = new QueryResponse() { Format = _defaultResponseFormat }; } // Initialize the builder PCAxis.Paxiom.IPXModelBuilder builder = GetPXBuilder(language, db, tablePath); builder.DoNotApplyCurrentValueSet = true; // DoNotApplyCurrentValueSet means the "client that made the request" is an api(, not a GUI) so that // CNMM2.4 property DefaultInGUI (for Valueset/grouping) should not be used builder.SetPreferredLanguage(language); builder.BuildForSelection(); // Process selections List <PCAxis.Paxiom.Selection> selections; try { selections = PCAxisRepository.BuildSelections(builder, tableQuery); } catch (Exception ex) { var message = ex is HttpException ? ex.Message : "Parameter error"; cacheResponse.ResponseData = context.Response.ContentEncoding.GetBytes(Error(message, false)); context.SendJSONError(cacheResponse, 400, true); // Logs usage _usageLogger.Info(String.Format("url={0}, type=error, caller={1}, cached=false, message=Parameter error", context.Request.RawUrl, context.Request.UserHostAddress)); return; } // Check that the number of selected cells do not exceed the limit long cellCount = 1; foreach (var sel in selections) { if (sel.ValueCodes.Count > 0) { cellCount *= sel.ValueCodes.Count; } } if (cellCount > Settings.Current.FetchCellLimit) { Write403Response(context); return; } builder.BuildForPresentation(selections.ToArray()); // Output handling starts here context.Response.Clear(); IWebSerializer serializer = WebSerializerSwitch.GetSerializer(tableQuery.Response.Format); //serializer.Serialize(builder.Model, context.Response); serializer.Serialize(builder.Model, cacheResponse); //context.Response.AddHeader("Content-Type", cacheResponse.ContentType); //context.Response.OutputStream.Write(cacheResponse.ResponseData, 0, cacheResponse.ResponseData.Length); context.Send(cacheResponse, true); //Logs usage _usageLogger.Info(String.Format("url={0}, type=data, caller={3}, cached=false, format={1}, matrix-size={2}", context.Request.RawUrl, tableQuery.Response.Format, builder.Model.Data.MatrixSize, context.Request.UserHostAddress)); }
/// <summary> /// Serializes and sends table data back to the client /// </summary> /// <param name="context"></param> private void SendTableData(HttpContext context, string language, string db, string[] tablePath, ResponseBucket cacheResponse) { //// TODO: Limit data size? //string data = ""; //using (var stream = new StreamReader(context.Request.InputStream)) //{ // data = stream.ReadToEnd(); //} var tableQuery = JsonHelper.Deserialize <TableQuery>(cacheResponse.PostData) as TableQuery; if (tableQuery.Response == null || tableQuery.Response.Format == null) { tableQuery.Response = new QueryResponse() { Format = _defaultResponseFormat }; } // Initialize the builder PCAxis.Paxiom.IPXModelBuilder builder = GetPXBuilder(language, db, tablePath); builder.DoNotApplyCurrentValueSet = true; // DoNotApplyCurrentValueSet means the "client that made the request" is an api(, not a GUI) so that // CNMM2.4 property DefaultInGUI (for Valueset/grouping) should not be used builder.SetPreferredLanguage(language); builder.BuildForSelection(); // Process selections var selections = PCAxisRepository.BuildSelections(builder, tableQuery); // Check that the number of selected cells do not exceed the limit long cellCount = 1; foreach (var sel in selections) { if (sel.ValueCodes.Count > 0) { cellCount *= sel.ValueCodes.Count; } } if (cellCount > Settings.Current.FetchCellLimit) { Write403Response(context); return; } builder.BuildForPresentation(selections.ToArray()); // Output handling starts here context.Response.Clear(); IWebSerializer serializer; switch (tableQuery.Response.Format != null ? tableQuery.Response.Format.ToLower() : null) { case null: case "px": serializer = new PxSerializer(); break; case "csv": serializer = new CsvSerializer(); break; case "json": serializer = new JsonSerializer(); break; case "json-stat": serializer = new JsonStatSeriaizer(); break; case "json-stat2": serializer = new JsonStat2Seriaizer(); break; case "xlsx": serializer = new XlsxSerializer(); break; //case "png": // int? width = tableQuery.Response.GetParamInt("width"); // int? height = tableQuery.Response.GetParamInt("height"); // string encoding = tableQuery.Response.GetParamString("encoding"); // serializer = new ChartSerializer(width, height, encoding); // break; case "sdmx": serializer = new SdmxDataSerializer(); break; default: throw new NotImplementedException("Serialization for " + tableQuery.Response.Format + " is not implemented"); } //serializer.Serialize(builder.Model, context.Response); serializer.Serialize(builder.Model, cacheResponse); //context.Response.AddHeader("Content-Type", cacheResponse.ContentType); //context.Response.OutputStream.Write(cacheResponse.ResponseData, 0, cacheResponse.ResponseData.Length); context.Send(cacheResponse, true); //Logs usage _usageLogger.Info(String.Format("url={0}, type=data, caller={3}, cached=false, format={1}, matrix-size={2}", context.Request.RawUrl, tableQuery.Response.Format, builder.Model.Data.MatrixSize, context.Request.UserHostAddress)); }
public static void SendJSONError(this HttpContext context, ResponseBucket data, int code, bool doCache) { data.HttpResponseCode = code; data.ContentType = "application/json"; Send(context, data, doCache); }