| | | 1 | | using AmbientServices.Extensions; |
| | | 2 | | using System; |
| | | 3 | | using System.Collections.Generic; |
| | | 4 | | using System.Text; |
| | | 5 | | |
| | | 6 | | namespace AmbientServices; |
| | | 7 | | |
| | | 8 | | /// <summary> |
| | | 9 | | /// A class that manages writing status notifications. |
| | | 10 | | /// </summary> |
| | | 11 | | /// <remarks> |
| | | 12 | | /// <pitch>Renders an organized status tree into two notifications at once: a terse plain-text form fit for SMS and a de |
| | | 13 | | /// <pledge> |
| | | 14 | | /// Callers drive it as a strictly-nested protocol: optionally enter html/body, then enter a status range, then optional |
| | | 15 | | /// <see cref="Terse"/> and <see cref="Details"/> may be read at any point and reflect everything written so far. The n |
| | | 16 | | /// </pledge> |
| | | 17 | | /// <plan> |
| | | 18 | | /// Two <see cref="StringBuilder"/>s built in parallel: the terse form uses indentation and the <see cref="StatusRating" |
| | | 19 | | /// </plan> |
| | | 20 | | /// </remarks> |
| | | 21 | | internal class StatusNotificationWriter |
| | | 22 | | { |
| | | 23 | | #if RAWRATINGS |
| | | 24 | | private const string DebugRatingFloatFormat = "0.00"; |
| | | 25 | | #endif |
| | | 26 | | private const string HtmlTabStart = "<div style=\"margin-left:10px\">"; |
| | | 27 | | private const string HtmlTabEnd = "</div>"; |
| | 2 | 28 | | private static readonly string[] RenderLevelStrings = new string[] { "h0", "h1", "h2", "h3", "h4" }; // note that |
| | | 29 | | private static string RenderLevelString(int level) |
| | | 30 | | { |
| | 2 | 31 | | return (level >= RenderLevelStrings.Length) ? "div" : RenderLevelStrings[level]; |
| | | 32 | | } |
| | 2 | 33 | | private static string RenderTerse(string terse) { return terse; } |
| | 2 | 34 | | private static string RenderDetails(string details) { return details; } |
| | | 35 | | |
| | | 36 | | private readonly StringBuilder _details; |
| | | 37 | | private readonly StringBuilder _terse; |
| | | 38 | | private int _tabLevel; |
| | | 39 | | private StatusRatingRange _currentSectionRatingRange; |
| | | 40 | | private DateTime? _notificationTime; |
| | | 41 | | |
| | | 42 | | /// <summary> |
| | | 43 | | /// Get the detailed notification (so far). |
| | | 44 | | /// </summary> |
| | 2 | 45 | | public string Details => _details.ToString(); |
| | | 46 | | /// <summary> |
| | | 47 | | /// Get the terse notification (so far). |
| | | 48 | | /// </summary> |
| | 2 | 49 | | public string Terse => _terse.ToString(); |
| | | 50 | | |
| | | 51 | | /// <summary> |
| | | 52 | | /// Constructs a status notification writer with the specified notification time. |
| | | 53 | | /// </summary> |
| | | 54 | | /// <param name="notificationTime">The notification time. Defaults to the current time.</param> |
| | 2 | 55 | | public StatusNotificationWriter(DateTime? notificationTime = null) |
| | | 56 | | { |
| | 2 | 57 | | _tabLevel = 1; |
| | 2 | 58 | | _currentSectionRatingRange = (StatusRatingRange)(-1); |
| | 2 | 59 | | _details = new StringBuilder(); |
| | 2 | 60 | | _terse = new StringBuilder(); |
| | 2 | 61 | | _notificationTime = notificationTime ?? AmbientClock.UtcNow; |
| | 2 | 62 | | } |
| | | 63 | | |
| | | 64 | | private static void OpenHeader(StringBuilder details, StringBuilder terse, int tabLevel, StatusRatingRange range = ( |
| | | 65 | | { |
| | 2 | 66 | | if (terse != null) |
| | | 67 | | { |
| | 2 | 68 | | terse.Append(new string(' ', tabLevel - 1)); |
| | 2 | 69 | | if (range != (StatusRatingRange)(-1)) |
| | | 70 | | { |
| | 2 | 71 | | string symbolPrefix = StatusRating.GetRangeSymbol(range) + " "; |
| | 2 | 72 | | terse.Append(symbolPrefix); |
| | | 73 | | } |
| | | 74 | | } |
| | 2 | 75 | | details.Append('<'); |
| | 2 | 76 | | details.Append(RenderLevelString(tabLevel)); |
| | 2 | 77 | | if (!string.IsNullOrEmpty(color)) details.Append(" style=\"color:" + color + "\""); |
| | 2 | 78 | | details.Append('>'); |
| | 2 | 79 | | } |
| | | 80 | | private static void CloseHeader(StringBuilder details, StringBuilder terse, int tabLevel) |
| | | 81 | | { |
| | 2 | 82 | | if (terse != null) terse.AppendLine(); |
| | 2 | 83 | | details.Append("</"); |
| | 2 | 84 | | details.Append(RenderLevelString(tabLevel)); |
| | 2 | 85 | | details.Append('>'); |
| | 2 | 86 | | } |
| | | 87 | | private void EnterTabLevel() |
| | | 88 | | { |
| | 2 | 89 | | _details.Append(HtmlTabStart); |
| | 2 | 90 | | ++_tabLevel; |
| | 2 | 91 | | } |
| | | 92 | | private void LeaveTabLevel() |
| | | 93 | | { |
| | 2 | 94 | | --_tabLevel; |
| | 2 | 95 | | _details.Append(HtmlTabEnd); |
| | 2 | 96 | | } |
| | | 97 | | /// <summary> |
| | | 98 | | /// Enters the html and body elements, with a background color set if the overall rating is specified. |
| | | 99 | | /// Should be matched by a subsequent call to <see cref="LeaveBodyAndHtml"/>. |
| | | 100 | | /// </summary> |
| | | 101 | | /// <param name="overallRating">The overall rating to use for the background color.</param> |
| | | 102 | | public void EnterHtmlAndBody(float? overallRating = null) |
| | | 103 | | { |
| | 2 | 104 | | _details.Append( |
| | 2 | 105 | | @"<html> |
| | 2 | 106 | | <head>"); |
| | 2 | 107 | | if (overallRating != null) |
| | | 108 | | { |
| | 2 | 109 | | string backgroundColor = StatusRating.GetRangeBackgroundColor(overallRating.Value); |
| | 2 | 110 | | _details.Append(@" |
| | 2 | 111 | | <style>"); |
| | 2 | 112 | | _details.Append(StatusRating.StyleDefinition); |
| | 2 | 113 | | _details.Append(@" |
| | 2 | 114 | | body{background-color:" + backgroundColor + @";} |
| | 2 | 115 | | </style>"); |
| | | 116 | | } |
| | 2 | 117 | | _details.Append(@" |
| | 2 | 118 | | </head> |
| | 2 | 119 | | <body> |
| | 2 | 120 | | "); |
| | 2 | 121 | | } |
| | | 122 | | /// <summary> |
| | | 123 | | /// Leaves the body and html tags entered in a previous call to <see cref="EnterHtmlAndBody"/>. |
| | | 124 | | /// </summary> |
| | | 125 | | public void LeaveBodyAndHtml() |
| | | 126 | | { |
| | 2 | 127 | | _details.Append(@" |
| | 2 | 128 | | </body> |
| | 2 | 129 | | </html> |
| | 2 | 130 | | "); |
| | 2 | 131 | | } |
| | | 132 | | /// <summary> |
| | | 133 | | /// Enters a status range section for the range indicated by the rating. |
| | | 134 | | /// Should be matched by a subsequent call to <see cref="LeaveStatusRange"/>. |
| | | 135 | | /// </summary> |
| | | 136 | | /// <param name="rating">The status rating.</param> |
| | | 137 | | public void EnterStatusRange(float rating) |
| | | 138 | | { |
| | 2 | 139 | | StatusRatingRange range = StatusRating.FindRange(rating); |
| | 2 | 140 | | string rangeName = StatusRating.GetRangeName(range); |
| | 2 | 141 | | string rangeColor = StatusRating.GetRangeForegroundColor(range); |
| | | 142 | | |
| | 2 | 143 | | if (_tabLevel > 1) throw new InvalidOperationException("All targets and status ranges must be closed before ente |
| | | 144 | | |
| | 2 | 145 | | _details.Append("<div class=\""); |
| | | 146 | | #pragma warning disable CA1308 // this is to convert the range name from the C# style casing (Pascal) to the HTML style |
| | 2 | 147 | | _details.Append(rangeName.ToLowerInvariant()); |
| | | 148 | | #pragma warning restore CA1308 |
| | 2 | 149 | | _details.Append("-range\">"); |
| | | 150 | | |
| | 2 | 151 | | OpenHeader(_details, _terse, _tabLevel, range, rangeColor); |
| | 2 | 152 | | _currentSectionRatingRange = range; |
| | | 153 | | |
| | 2 | 154 | | _terse.Append(rangeName.ToUpperInvariant()); |
| | 2 | 155 | | _details.Append(StatusRating.GetRangeSymbol(range)); |
| | 2 | 156 | | _details.Append(' '); |
| | 2 | 157 | | _details.Append(rangeName); |
| | | 158 | | |
| | 2 | 159 | | if (_notificationTime != null) |
| | | 160 | | { |
| | 2 | 161 | | _terse.Append(" @"); |
| | 2 | 162 | | _terse.Append(_notificationTime.Value.ToShortTimeString()); |
| | 2 | 163 | | _details.Append(" at "); |
| | 2 | 164 | | _details.Append(_notificationTime.Value.ToLongTimeString()); |
| | 2 | 165 | | _notificationTime = null; |
| | | 166 | | } |
| | | 167 | | |
| | 2 | 168 | | CloseHeader(_details, _terse, _tabLevel); |
| | | 169 | | |
| | 2 | 170 | | EnterTabLevel(); |
| | 2 | 171 | | } |
| | | 172 | | /// <summary> |
| | | 173 | | /// Leaves a status range started by a call to <see cref="EnterStatusRange"/>. |
| | | 174 | | /// </summary> |
| | | 175 | | public void LeaveStatusRange() |
| | | 176 | | { |
| | 2 | 177 | | if (_tabLevel != 2) throw new InvalidOperationException("There is no status range to leave!"); |
| | 2 | 178 | | LeaveTabLevel(); |
| | 2 | 179 | | _details.Append("</div>"); |
| | 2 | 180 | | } |
| | | 181 | | /// <summary> |
| | | 182 | | /// Enters a target section. Should be matched by a subsequent call to <see cref="LeaveTarget"/>. |
| | | 183 | | /// </summary> |
| | | 184 | | /// <param name="target">The name of the target owning this section.</param> |
| | | 185 | | /// <param name="rating">The overall status rating for this target.</param> |
| | | 186 | | public void EnterTarget(string target, float rating) |
| | | 187 | | { |
| | 2 | 188 | | if (_tabLevel < 2) throw new InvalidOperationException("A status range must be entered before a target is!"); |
| | | 189 | | |
| | 2 | 190 | | string rgbColor = StatusRating.GetRatingRgbForegroundColor(rating); |
| | 2 | 191 | | StatusRatingRange range = StatusRating.FindRange(rating); |
| | 2 | 192 | | OpenHeader(_details, _terse, _tabLevel, (StatusRatingRange)(-1), rgbColor); |
| | 2 | 193 | | _terse.Append(target); |
| | 2 | 194 | | _details.Append(target); |
| | 2 | 195 | | CloseHeader(_details, _terse, _tabLevel); |
| | 2 | 196 | | EnterTabLevel(); |
| | 2 | 197 | | } |
| | | 198 | | |
| | | 199 | | /// <summary> |
| | | 200 | | /// Leaves a target section entered by a previous call to <see cref="EnterTarget"/>. |
| | | 201 | | /// </summary> |
| | | 202 | | public void LeaveTarget() |
| | | 203 | | { |
| | 2 | 204 | | if (_tabLevel < 3) throw new InvalidOperationException("There is no target to leave!"); |
| | | 205 | | |
| | 2 | 206 | | LeaveTabLevel(); |
| | 2 | 207 | | } |
| | | 208 | | /// <summary> |
| | | 209 | | /// Writes a notification for the specified <see cref="AggregatedAlert"/>. |
| | | 210 | | /// </summary> |
| | | 211 | | /// <param name="aggregatedAlert">An <see cref="AggregatedAlert"/> that should be written to the notification.</para |
| | | 212 | | public void WriteAggregatedAlert(AggregatedAlert aggregatedAlert) |
| | | 213 | | { |
| | 2 | 214 | | if (_tabLevel <= 1) throw new InvalidOperationException("A status range must be entered before aggregated alerts |
| | | 215 | | |
| | 2 | 216 | | StatusAuditAlert auditAlert = aggregatedAlert.CommonAlert ?? StatusAuditAlert.None; |
| | 2 | 217 | | StatusRatingRange range = StatusRating.FindRange(auditAlert.Rating); |
| | 2 | 218 | | string rangeName = StatusRating.GetRangeName(range); |
| | 2 | 219 | | string rangeColor = StatusRating.GetRangeForegroundColor(auditAlert.Rating); |
| | 2 | 220 | | string rgbColor = StatusRating.GetRatingRgbForegroundColor(aggregatedAlert.RatingSum / aggregatedAlert.Sources.C |
| | | 221 | | |
| | 2 | 222 | | List<StatusPropertyRange> propertyRanges = aggregatedAlert.PropertyRanges; |
| | | 223 | | |
| | 2 | 224 | | OpenHeader(_details, _terse, _tabLevel, (StatusRatingRange)(-1), rgbColor); |
| | | 225 | | |
| | 2 | 226 | | _terse.Append(aggregatedAlert.Target + ": " + aggregatedAlert.TerseSources + "->"); |
| | | 227 | | |
| | | 228 | | #if RAWRATINGS |
| | | 229 | | _details.Append("[RATING=" + auditAlert.Rating.ToString(DebugRatingFloatFormat) + "] "); |
| | | 230 | | #endif |
| | 2 | 231 | | _details.Append(aggregatedAlert.Target + ": " + aggregatedAlert.DetailsSources + " reporting: "); |
| | | 232 | | |
| | | 233 | | // multi-line alert details or properties to list? |
| | 2 | 234 | | if ((aggregatedAlert.CommonAlert?.Details != null && (aggregatedAlert.CommonAlert.Details.Contains("<br/>", Stri |
| | | 235 | | { |
| | 2 | 236 | | CloseHeader(_details, _terse, _tabLevel); |
| | | 237 | | |
| | 2 | 238 | | EnterTabLevel(); |
| | 2 | 239 | | OpenHeader(_details, _terse, _tabLevel, (StatusRatingRange)(-1), rgbColor); |
| | | 240 | | |
| | 2 | 241 | | _terse.Append(RenderTerse(auditAlert.Terse).Replace("\n", "\n" + new string(' ', _tabLevel), StringCompariso |
| | 2 | 242 | | _details.AppendLine(RenderDetails(auditAlert.Details)); |
| | | 243 | | |
| | 2 | 244 | | CloseHeader(_details, _terse, _tabLevel); |
| | | 245 | | |
| | | 246 | | // are there properties? |
| | 2 | 247 | | if (propertyRanges != null && propertyRanges.Count > 0) |
| | | 248 | | { |
| | 2 | 249 | | _details.Append(" because"); |
| | 2 | 250 | | foreach (StatusPropertyRange propertyRange in aggregatedAlert.PropertyRanges) |
| | | 251 | | { |
| | 2 | 252 | | OpenHeader(_details, _terse, _tabLevel, (StatusRatingRange)(-1), rgbColor); |
| | 2 | 253 | | _terse.Append(propertyRange.ToString()); |
| | 2 | 254 | | _details.AppendLine(propertyRange.ToString()); |
| | 2 | 255 | | CloseHeader(_details, _terse, _tabLevel); |
| | | 256 | | } |
| | | 257 | | } |
| | 2 | 258 | | LeaveTabLevel(); |
| | | 259 | | } |
| | | 260 | | else |
| | | 261 | | { |
| | 2 | 262 | | _terse.Append(RenderTerse(auditAlert.Terse)); |
| | 2 | 263 | | _details.Append(' '); |
| | 2 | 264 | | _details.Append(RenderDetails(auditAlert.Details)); |
| | 2 | 265 | | CloseHeader(_details, _terse, _tabLevel); |
| | | 266 | | } |
| | 2 | 267 | | } |
| | | 268 | | } |