GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
GPALElementConverter.cs
1// =============================================================================
2// GPAL - Generally Positive Automation Library
3// Copyright © 2026 Software Decisions, Inc. All rights reserved.
4//
5// This file is part of GPAL.
6// Licensed under the Business Source License 1.1
7//
8// Primary development, architecture, and vision by Michael B. Vederman,
9// CEO of Software Decisions, Inc., Texas.
10//
11// Internal development maintained privately.
12// Public releases appear on GitHub: https://github.com/SoftwareDecisionsInc/GPAL.
13//
14// See LICENSE for full terms, including Additional Use Grant.
15// =============================================================================
16
17using System;
18using System.Collections.Generic;
19using System.Drawing;
20using System.Linq;
21using Newtonsoft.Json;
22using Newtonsoft.Json.Linq;
23using static GenerallyPositive.Enums;
24
25namespace GenerallyPositive
26{
27 internal class GPALElementConverter : JsonConverter
28 {
29 public override bool CanConvert(Type objectType)
30 {
31 return objectType == typeof(GPALElement) || objectType == typeof(List<GPALElement>);
32 }
33
34 public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
35 {
36 JToken token;
37 try
38 {
39 token = JToken.Load(reader);
40 }
41 catch (Exception ex)
42 {
43 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to load JSON token", null, GPALObjectType.None, ex);
44 return objectType == typeof(List<GPALElement>) ? new List<GPALElement>() : null;
45 }
46
47 if (token.Type == JTokenType.Array)
48 {
49 // Handle List<GPALElement>
50 var jsonArray = (JArray)token;
51 var result = new List<GPALElement>();
52
53 //GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"Processing JSON array with [{jsonArray.Count}] elements");
54
55 foreach (JToken maybeWrapped in jsonArray)
56 {
57 JToken payload = maybeWrapped;
58
59 // 1. Unwrap extra array level if present (common for single-element queries)
60 if (maybeWrapped.Type == JTokenType.Array && maybeWrapped.Children().Count() == 1)
61 {
62 payload = maybeWrapped.First;
63 //GPAL.PublishSimpleEvent(Enums.GPALEventType.DEBUG, "Unwrapped extra array layer");
64 }
65
66 GPALElement element = null;
67
68 if (payload.Type == JTokenType.Object)
69 {
70 // Normal structured object > deserialize as usual
71 try
72 {
73 element = DeserializeSingleElement((JObject)payload, serializer);
74 }
75 catch (Exception ex)
76 {
77 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION,
78 "Normal object failed: " + ex.Message);
79 }
80 }
81 else if (payload.Type == JTokenType.String)
82 {
83 string raw = payload.Value<string>();
84 if (string.IsNullOrWhiteSpace(raw))
85 continue;
86
87 if (raw == "no elements found")
88 continue;
89
90 // Quick check: is this the broken double-escaped head?
91 if (raw.StartsWith("{\"Attributes\":") && raw.Contains("\"tagName\":\"head\""))
92 {
93 GPAL.PublishSimpleEvent(Enums.GPALEventType.WARNING,
94 "Detected broken escaped <head> string — attempting surgical repair");
95
96 try
97 {
98 // Try naive parse first
99 var parsed = JObject.Parse(raw);
100 element = DeserializeSingleElement(parsed, serializer);
101 }
102 catch (JsonReaderException)
103 {
104 // Repair attempt: cut off after the last reliable field (pageURL)
105 int endIndex = raw.LastIndexOf("\"pageURL\":");
106 if (endIndex >= 0)
107 {
108 // Include closing " and } of pageURL, plus outer }
109 // This is approximate — adjust offset if pageURL value changes length/format
110 int safeEnd = raw.IndexOf("\"", endIndex + 10) + 1; // after value
111 if (safeEnd > endIndex)
112 {
113 string cleaned = raw.Substring(0, safeEnd) + "}}";
114
115 try
116 {
117 var fixedObj = JObject.Parse(cleaned);
118 element = DeserializeSingleElement(fixedObj, serializer);
119 }
120 catch (Exception ex2)
121 {
122 GPAL.PublishSimpleEvent(Enums.GPALEventType.WARNING,
123 "Repair parse failed: " + ex2.Message);
124 }
125 }
126 }
127
128 // Ultimate fallback: build minimal element manually
129 if (element == null || element.Attributes == null)
130 {
131 GPAL.PublishSimpleEvent(Enums.GPALEventType.WARNING,
132 "Repair failed — creating minimal head element with known fields");
133
134 element = new GPALElement();
135 element.Attributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
136 element.Attributes["tagName"] = "head";
137
138 // Try to extract xpath, css, fullXPath manually if present early in string
139 if (raw.Contains("\"xpath\":"))
140 {
141 var match = System.Text.RegularExpressions.Regex.Match(raw, @"""xpath"":""([^""]+)""");
142 if (match.Success && match.Groups.Count > 1)
143 element.Attributes["xpath"] = match.Groups[1].Value;
144 }
145
146 if (raw.Contains("\"css\":"))
147 {
148 var match = System.Text.RegularExpressions.Regex.Match(raw, @"""css"":""([^""]+)""");
149 if (match.Success && match.Groups.Count > 1)
150 element.Attributes["css"] = match.Groups[1].Value;
151 }
152
153 if (raw.Contains("\"fullXPath\":"))
154 {
155 var match = System.Text.RegularExpressions.Regex.Match(raw, @"""fullXPath"":""([^""]+)""");
156 if (match.Success && match.Groups.Count > 1)
157 element.Attributes["fullXPath"] = match.Groups[1].Value;
158 }
159
160 // You can add more fields (id, class, etc.) the same way if needed
161 }
162 }
163 }
164 else if (raw.TrimStart().StartsWith("{"))
165 {
166 // Not head, but looks like JSON — normal attempt
167 try
168 {
169 var obj = JObject.Parse(raw);
170 element = DeserializeSingleElement(obj, serializer);
171 }
172 catch (JsonReaderException jex)
173 {
174 GPAL.PublishSimpleEvent(Enums.GPALEventType.WARNING,
175 "Non-head string failed parse: " + jex.Message);
176 }
177 }
178 }
179
180 // Final check — only add if we have something useful
181 if (element != null && element.Attributes != null && element.Attributes.Count > 0)
182 {
183 // commented: fired per element during bulk deserialize
184 //string tag = element.Attributes.ContainsKey("tagName")
185 // ? element.Attributes["tagName"].ToString()
186 // : "unknown";
187
188 //GPAL.PublishSimpleEvent(Enums.GPALEventType.DEEPDEBUG,
189 // "Added element - tag: " + tag + ", attributes: " + element.Attributes.Count);
190
191 result.Add(element);
192 }
193 else
194 {
195 GPAL.PublishSimpleEvent(Enums.GPALEventType.WARNING,
196 "Dropped item — no valid element created");
197 }
198 }
199
200 return result;
201 }
202 else if (token.Type == JTokenType.Object)
203 {
204 // Handle single GPALElement
205 try
206 {
207 var element = DeserializeSingleElement((JObject)token, serializer);
208 if (element != null && element.Attributes != null)
209 {
210 return element;
211 }
212 else
213 {
214 return null;
215 }
216 }
217 catch (Exception ex)
218 {
219 return null;
220 }
221 }
222 else if (token.Type == JTokenType.String)
223 {
224 // Handle single double-serialized GPALElement
225 try
226 {
227 var jsonString = token.Value<string>();
228 if (jsonString == "no elements found")
229 {
230 return objectType == typeof(List<GPALElement>) ? new List<GPALElement>() : null;
231 }
232 var parsedObject = JObject.Parse(jsonString);
233 var element = DeserializeSingleElement(parsedObject, serializer);
234 if (element != null && element.Attributes != null)
235 {
236 return element;
237 }
238 else
239 {
240 return null;
241 }
242 }
243 catch (Exception ex)
244 {
245 return objectType == typeof(List<GPALElement>) ? new List<GPALElement>() : null;
246 }
247 }
248
249 return objectType == typeof(List<GPALElement>) ? new List<GPALElement>() : null;
250 }
251 private GPALElement DeserializeSingleElement(JObject jsonObject, JsonSerializer serializer)
252 {
253 var gpalelement = new GPALElement();
254
255 // Deserialize Attributes dictionary
256 if (jsonObject["Attributes"] != null)
257 {
258 var attributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
259 var attributesJson = jsonObject["Attributes"] as JObject;
260
261 if (attributesJson != null)
262 {
263 foreach (var prop in attributesJson.Properties())
264 {
265 try
266 {
267 switch (prop.Name.ToLower())
268 {
269 case "location":
270 var location = prop.Value.ToObject<Point>(serializer);
271 attributes["location"] = location;
272 // commented: fired per element during bulk deserialize; the eager string build (and its stack capture) cost seconds over 90+ elements
273 //GPAL.PublishSimpleEvent(GPALEventType.DEEPDEBUG, $"Deserialized location: ([{location.X}], [{location.Y}])");
274 break;
275 case "size":
276 var size = prop.Value.ToObject<Size>(serializer);
277 attributes["size"] = size;
278 //GPAL.PublishSimpleEvent(GPALEventType.DEEPDEBUG, $"Deserialized size: ([{size.Width}], [{size.Height}])");
279 break;
280 case "boundingrect":
281 var boundingRect = prop.Value.ToObject<ClientRectangle>(serializer);
282 attributes["boundingRect"] = boundingRect;
283 //GPAL.PublishSimpleEvent(GPALEventType.DEEPDEBUG, $"Deserialized boundingRect: (width=[{boundingRect.Width}], height=[{boundingRect.Height}])");
284 break;
285 case "xpaths":
286 var xpaths = prop.Value.ToObject<List<XPathInfo>>(serializer);
287 attributes["xpaths"] = xpaths.FindAll(x => x != null).ToArray();
288 //GPAL.PublishSimpleEvent(GPALEventType.DEEPDEBUG, $"Deserialized attributes['xpaths']");
289 break;
290 default:
291 attributes[prop.Name] = prop.Value.ToObject<object>(serializer);
292 // commented: fired per PROPERTY per element; {prop.Value} also re-serializes the token to a string every time
293 //GPAL.PublishSimpleEvent(GPALEventType.DEEPDEBUG, $"Deserialized property [{prop.Name}]: [{prop.Value}]");
294 break;
295 }
296 }
297 catch (Exception ex)
298 {
299 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to deserialize property [{prop.Name}]", null, GPALObjectType.None, ex);
300 }
301 }
302 gpalelement.Attributes = attributes;
303 }
304 else
305 {
306 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "Attributes property is not a valid JObject");
307 }
308 }
309 else
310 {
311 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "Attributes property is missing or null");
312 }
313
314 // Deserialize ElementHandle
315 if (jsonObject["ElementHandle"] != null)
316 {
317 try
318 {
319 gpalelement.ElementHandle = jsonObject["ElementHandle"].ToObject<string>(serializer);
320 //GPAL.PublishSimpleEvent(GPALEventType.DEEPDEBUG, $"Deserialized ElementHandle: [{gpalelement.ElementHandle}]");
321 }
322 catch (Exception ex)
323 {
324 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to deserialize ElementHandle", null, GPALObjectType.None, ex);
325 }
326 }
327
328 return gpalelement;
329 }
330
331 public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
332 {
333 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Serialization not implemented");
334 }
335
336 public override bool CanWrite => false;
337 }
338
339 internal class PointConverter : JsonConverter
340 {
341 public override bool CanConvert(Type objectType)
342 {
343 return objectType == typeof(Point);
344 }
345
346 public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
347 {
348 try
349 {
350 JToken token = JToken.Load(reader);
351 if (token.Type == JTokenType.Object)
352 {
353 var obj = (JObject)token;
354 int x = obj["X"]?.Value<int>() ?? 0;
355 int y = obj["Y"]?.Value<int>() ?? 0;
356 return new Point(x, y);
357 }
358 else if (token.Type == JTokenType.String)
359 {
360 string[] parts = token.Value<string>().Split(',');
361 if (parts.Length == 2 && int.TryParse(parts[0], out int x) && int.TryParse(parts[1], out int y))
362 {
363 return new Point(x, y);
364 }
365 }
366 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid Point format: [{token}]");
367 return Point.Empty;
368 }
369 catch (Exception ex)
370 {
371 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to deserialize Point", null, GPALObjectType.None, ex);
372 return Point.Empty;
373 }
374 }
375
376 public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
377 {
378 var point = (Point)value;
379 writer.WriteValue($"{point.X},{point.Y}");
380 }
381
382 public override bool CanWrite => true;
383 }
384
385 internal class SizeConverter : JsonConverter
386 {
387 public override bool CanConvert(Type objectType)
388 {
389 return objectType == typeof(Size);
390 }
391
392 public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
393 {
394 try
395 {
396 JToken token = JToken.Load(reader);
397 if (token.Type == JTokenType.Object)
398 {
399 var obj = (JObject)token;
400 int width = obj["Width"]?.Value<int>() ?? 0;
401 int height = obj["Height"]?.Value<int>() ?? 0;
402 return new Size(width, height);
403 }
404 else if (token.Type == JTokenType.String)
405 {
406 string[] parts = token.Value<string>().Split(',');
407 if (parts.Length == 2 && int.TryParse(parts[0], out int width) && int.TryParse(parts[1], out int height))
408 {
409 return new Size(width, height);
410 }
411 }
412 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid Size format: [{token}]");
413 return Size.Empty;
414 }
415 catch (Exception ex)
416 {
417 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to deserialize Size", null, GPALObjectType.None, ex);
418 return Size.Empty;
419 }
420 }
421
422 public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
423 {
424 var size = (Size)value;
425 writer.WriteValue($"{size.Width},{size.Height}");
426 }
427
428 public override bool CanWrite => true;
429 }
430}