001package Torello.Browser; 002 003import Torello.Java.ReadOnly.ReadOnlyList; 004import Torello.Java.ReadOnly.ReadOnlyMap; 005 006import java.util.stream.IntStream; 007 008import javax.json.JsonObject; // JavaDoc Needs this 009import javax.json.stream.JsonGenerator; 010 011/** 012 * This class is declared {@code 'abstract'} and serves as the root ancestor clas for all data 013 * record types & events in all <B STYLE='color:red;'><I>domains</I></B> in within the 014 * Chrome DevTools Protocol (CDP) API. 015 * 016 * <BR /><BR /> 017 * This class utilizes a "Singleton Design Pattern." Each of the types and events in CDP 018 * ({@code 'Torello.Browser.*'}) are declared as nested classes within a parent or "container" 019 * class, rather than as standalone types. This decision helps make clear which types belong to 020 * which domains in CDP. 021 * 022 * <BR /><BR /> 023 * Programatically, it would likely be a little bit more "clear" to have a separate Java Package 024 * for Google Domain. In fact, within the Web-Browser Source-Code, a "domain" is essentially 025 * idential to the word "Java Package." What's the catch? Owing to the fact that the domains, 026 * types & events in the CDP-API (this Java-Wrapper) are nothing more than "Wrapper Code" that 027 * sends JSON requests to the browser, and parses responses from the browser. 028 * 029 * <BR /><BR /><DIV CLASS=JDHint> 030 * Essentially, the classes, methods & types in the Browser & JavaScript API's are just 031 * "messengers" to and from Google Chrome. As such, the actual source code for these "domains" 032 * is not in Java, but inside Chrome, itself. Thusly, compacting all of the types & events 033 * into a <B STYLE='color:red'>single Java Class</B> (one class per domain) seems an intelligent 034 * design choice. 035 * </DIV> 036 * 037 * <BR /> 038 * Each of the types & in every domain of the CDP uses this abstract class as its root 039 * ancestor. 040 * 041 */ 042public abstract class BaseType<DOMAIN_NESTED extends BaseType<DOMAIN_NESTED>> 043 implements Comparable<BaseType<?>> 044{ 045 // Don't ask. Chat-GPT convinced me this is the "best way" to do this... It is not that 046 // important. It is just annoying enough to look (at least for me) to warrant / justify this 047 // pointless comment. 048 // 049 // "THIS" is needed to pass 'this' to each of the helper methods inside of the helper singleton 050 // class. If you look at the one line method bodies for equals, hashCode, toJSON and fromJSON 051 // you will see 'THIS' field being used! 052 053 @SuppressWarnings("unchecked") 054 private final DOMAIN_NESTED THIS = (DOMAIN_NESTED) this; 055 056 057 // ******************************************************************************************** 058 // ******************************************************************************************** 059 // Constructor 060 // ******************************************************************************************** 061 // ******************************************************************************************** 062 063 064 /** Constructor for this {@code abstract-class} */ 065 protected BaseType( 066 final NestedHelper<DOMAIN_NESTED> helperSingleton, 067 final Domains domain, 068 final String name, 069 final int numFields 070 ) 071 { 072 this.helperSingleton = helperSingleton; 073 this.domain = domain; 074 this.name = name; 075 this.numFields = numFields; 076 } 077 078 079 // ******************************************************************************************** 080 // ******************************************************************************************** 081 // Instance Fields 082 // ******************************************************************************************** 083 // ******************************************************************************************** 084 085 086 /** 087 * Singleton Instance of the Nested Helper Class. This is how the "Singleton Design Pattern" 088 * is implemented. The methods & fields in this class implement many of the features 089 * offered by abstract class {@code 'BaseType'}. 090 */ 091 public final NestedHelper<DOMAIN_NESTED> helperSingleton; 092 093 /** 094 * It was decided by somebody other than I that there are to be two API's of the browser 095 * Remote-Debug-Port interface. The two API's were decided to be the {@code JavaScript} 096 * API, and the {@code Browser} API. These two do not have a lot of distinction or meaning. 097 * 098 * <BR /><BR />💡 Each API has several categories of methods, and these are called 099 * {@code Domain's}. 100 */ 101 public final Domains domain; 102 103 /** 104 * The event has a name, and this name happens to be the exact same name as the 105 * event-{@code class} itself. 106 */ 107 public final String name; 108 109 /** 110 * This is the number of fields in this class. It is like a reflection-field reflecting 111 * information for the concrete subclass instance. 112 */ 113 public final int numFields; 114 115 116 /** 117 * The {@code 'isPresent'} boolean-array cannot be assigned inside of this abstract ancestor 118 * class' constructor. Thus, the only other option for actually using or retrieving the values 119 * in this list is to include a simple {@code 'getter'} or {@code 'accessor'} method. See 120 * method: {@link #isPresent()} 121 * 122 * <BR /><BR /> 123 * The {@code 'isPresent'} list cannot be assigned until after the fields of the concrete 124 * instance sub-class have been assigned. Since it was not until JDK 22+ that statements 125 * prior to the invocation of {@code super()} were allowed inside of constructors, this field 126 * shall simply remain {@code 'protected'} and inaccessible to outside modification. 127 * 128 * @see #isPresent() 129 */ 130 public ReadOnlyList<Boolean> isPresent; 131 132 133 134 // ******************************************************************************************** 135 // ******************************************************************************************** 136 // java.lang.Object, java.lang.Comparable 137 // ******************************************************************************************** 138 // ******************************************************************************************** 139 140 141 /** 142 * Checks the Contents of this class again the input parameter {@code 'other'} for euality. 143 * <EMBED CLASS='external-html' DATA-FILE-ID=BaseType.HelperSingletonNote> 144 * @return {@code 'TRUE'} if and only if the two instances are equal. 145 * @see NestedHelper#equals(BaseType, Object) 146 */ 147 public final boolean equals(Object other) 148 { return helperSingleton.equals(THIS, other); } 149 150 /** 151 * Generates a Hash-Code for this class, which may be used for hashing into maps and sets. 152 * <EMBED CLASS='external-html' DATA-FILE-ID=BaseType.HelperSingletonNote> 153 * @return a valid hash code for this object. 154 * @see NestedHelper#hashCode(BaseType) 155 */ 156 public final int hashCode() 157 { return helperSingleton.hashCode(THIS); } 158 159 /** 160 * This method uses Java Reflection to convert the inheriting object into a {@code String}. 161 * @return A {@code java.lang.String} representation of {@code 'this'} object 162 */ 163 public final String toString() 164 { return CDPToString.toString(THIS); } 165 166 /** 167 * Compares this {@code BaseType} to another for ordering. 168 * 169 * <BR /><BR /> 170 * Comparison is performed in two steps: 171 * 172 * <BR /><BR /><OL CLASS=JDOL> 173 * <LI>First by {@code domain}</LI> 174 * <LI>Then by {@code name} if domains are equal</LI> 175 * </OL> 176 * 177 * @param o The {@code BaseType} to be compared. 178 * 179 * @return A negative integer, zero, or a positive integer as this object is less 180 * than, equal to, or greater than the specified object. 181 * 182 * @throws NullPointerException If {@code o} is {@code null}. 183 */ 184 @Override 185 public final int compareTo(final BaseType<?> o) 186 { 187 final int i = this.domain.compareTo(o.domain); 188 if (i != 0) return i; 189 190 return this.name.compareTo(o.name); 191 } 192 193 194 // ******************************************************************************************** 195 // ******************************************************************************************** 196 // Json Serialization Method 197 // ******************************************************************************************** 198 // ******************************************************************************************** 199 200 201 /** 202 * Serializes {@code 'this'} object into JSON. 203 * 204 * <BR /><BR /><OL CLASS=JDOL> 205 * <LI>It is OK if 'name' is null.</LI> 206 * <LI>It is NOT-OK if 'jGen' is null.</LI> 207 * </OL> 208 * 209 * <EMBED CLASS='external-html' DATA-FILE-ID=BaseType.HelperSingletonNote> 210 * 211 * @param name The name being assigned to this {@link JsonObject} Property. This name may be 212 * null. If null is passed here, the {@link JsonGenerator} will treat this Object as the top 213 * level JSON Object, not a property of a larger object. 214 * 215 * @param jGen This is the generator instance. Note that often, when serializing Java Objects 216 * to JSON (<I>and vice-versa!</I>), one object may be a property or field of another object. 217 * When serializing an object to JSON, if there is a field that also needs to be serialized as 218 * a {@code JsonObject}, just pass the name and this generator instance in order to include 219 * that field as a sub-property. 220 * 221 * @see NestedHelper#toJSON(BaseType, String, JsonGenerator) 222 */ 223 public final void toJSON(final String name, final JsonGenerator jGen) 224 { helperSingleton.toJSON(THIS, name, jGen); } 225 226 227 // ******************************************************************************************** 228 // ******************************************************************************************** 229 // Data Validation / Integrity Methods 230 // ******************************************************************************************** 231 // ******************************************************************************************** 232 233 234 /** 235 * Checks whether the state of a type or event is actually valid, according to the CDP 236 * specifications, as per the field's "Optional Flag." Google's Json-Specifications File for 237 * the CDP-API declares that some fields (referred to as 'properties' in the CDP-API Json-Spec 238 * File) for some of the API's types & events are not actually mandatory or required. When 239 * Google Chrome (or another CDP compliant Web-Browser) returns a type, or fires / generates an 240 * event, the browser may choose to omit certain fields in the classes that are constructed and 241 * returned to you, the end user. 242 * 243 * <BR /><BR /><DIV ClASS=JDHint> 244 * Generally, such fields are assigned null. <B STYLE='color:red;'>This design choice explains 245 * why Java's Boxed-Types (such as {@code java.lang.Integer} may seem ubiquitous in this 246 * API</B>). Note that fields which are declared as simple types such as {@code 'int'} are 247 * indeed declared using a Java Primitive {@code 'int'}, not the boxed type 248 * {@code 'java.lang.Integer'}. 249 * </DIV> 250 * 251 * <BR />There are only two minor issues. One, Google Chrome is not under "developmental 252 * control" of "Torello Software." Though, theoretically, Chome would never omit a "non 253 * optional property", on the off chance that it has done so, this method can be 254 * used to scan for such an occurence. 255 * 256 * <BR /><BR /> 257 * Since both types and events can be constructed using their respective public constructors, 258 * if a user has omitted a field (Google doesn't use the term 'field' or 'class', instead it 259 * refers to such concepts as 'types', 'events', and 'properties'), then this method may als 260 * be used to validate user constructed objects. 261 * 262 * @return returns an {@code java.util.stream.IntStream} containing all indices of fields which 263 * meets these criteria: 264 * 265 * <BR /><BR /><UL CLASS=JDUL> 266 * <LI>hasn't been declared optional in the CDP specifications</LI> 267 * <LI>is asserted not to be present, as per the value of {@code 'this'} isPresentList</LI> 268 * </UL> 269 * 270 * <BR /><BR />If there are no such fields, then this method simply returns an empty 271 * {@code java.util.stream.IntStream}. 272 * 273 * @see NestedDescriptor#optionals 274 * @see #isPresent() 275 */ 276 public final IntStream optionalsValidate() 277 { 278 final IntStream.Builder b = IntStream.builder(); 279 final ReadOnlyList<Boolean> optionals = helperSingleton.descriptor().optionals; 280 final ReadOnlyList<Boolean> isPresent = this.isPresent(); 281 final int NUM = optionals.size(); 282 283 for (int i=0; i < NUM; i++) 284 if (! optionals.get(i)) // TRUE => 'optional', FALSE => 'mandatory' 285 if (!isPresent.get(i)) // TRUE => 'included', FALSE => 'omitted' 286 b.accept(i); 287 288 return b.build(); 289 } 290 291 /** 292 * Generates a properly formatted exception throw message using the output produced by 293 * {@link #optionalsValidate()}, and then throws {@link NullNonOptionalException}. 294 * 295 * <BR /><BR /><DIV CLASS=JDHint> 296 * If the output {@code IntStream} returned by {@code optionalsValidate} is empty, then this 297 * method simply exits, and returns gracefull - WITHOUT THROWING. 298 * </DIV> 299 * 300 * @throws NullNonOptionalException If the {@code IntStream} returned by 301 * {@code optionalsValidate()} has a non-zero length. 302 * 303 * @see #optionalsValidate() 304 * @see NestedDescriptor#optionals 305 * @see #isPresent() 306 */ 307 public void optionalsValidateThrow() 308 { 309 final int[] errors = optionalsValidate().toArray(); 310 311 if (errors.length == 0) return; 312 313 throw new NullNonOptionalException 314 (printHelper(errors, "isn't present, but also isn't optional")); 315 } 316 317 /** 318 * This method will investigate the contents of any & string fields in the concrete 319 * sub-type of {@code 'this'} instance for validity against pre-defined <B STYLE='color:red;'> 320 * enumerated string constants</B>. 321 * 322 * <BR /><BR />If {@code 'this'} concrete sub-type doesn't have any string fields among its 323 * public fields, or the string fields which it has have not been designted as members of a 324 * domain "enum type", then this method returns an empty {@code java.util.stream.IntStream}. 325 * 326 * <EMBED CLASS='external-html' DATA-FILE-ID=BaseType.HelperSingletonNote> 327 * 328 * @return returns an {@code java.util.stream.IntStream} containing all indices of fields which 329 * meets these criteria: 330 * 331 * <BR /><BR /><UL CLASS=JDUL> 332 * <LI>is declared as a CDP (or Java) <B STYLE='color:red;'><CODE>String</CODE></B></LI> 333 * <LI>has failed to abide by a pre-specified list of enumerated string constants</LI> 334 * </UL> 335 * 336 * <BR /><BR />If there are no such fields, then this method simply returns an empty 337 * {@code java.util.stream.IntStream}. 338 * 339 * @see NestedHelper#enumStrValidate(BaseType) 340 */ 341 public final IntStream enumStrValidate() 342 { return helperSingleton.enumStrValidate(THIS); } 343 344 /** 345 * Generates a properly formatted exception throw message using the output produced by 346 * {@link #optionalsValidate()}, and then throws {@link InvalidEnumStrException}. 347 * 348 * <BR /><BR /><DIV CLASS=JDHint> 349 * If the output {@code IntStream} returned by {@code enumStrValidate} is empty, then this 350 * method simply exits, and returns gracefull - WITHOUT THROWING. 351 * </DIV> 352 * 353 * @throws InvalidEnumStrException If the {@code IntStream} returned by 354 * {@code optionalsValidate()} has a non-zero length. 355 * 356 * @see #enumStrValidate() 357 * @see NestedHelper#enumStrValidate(BaseType) 358 */ 359 public void enumStrValidateThrow() 360 { 361 final int[] errors = enumStrValidate().toArray(); 362 363 if (errors.length == 0) return; 364 365 throw new InvalidEnumStrException 366 (printHelper(errors, "has an invalid string value")); 367 } 368 369 private String printHelper(final int[] errors, final String msg) 370 { 371 final StringBuilder sb = new StringBuilder(); 372 final NestedDescriptor<DOMAIN_NESTED> descriptor = helperSingleton.descriptor(); 373 final ReadOnlyList<String> names = descriptor.names; 374 375 sb.append("In class: " + this.getClass().getSimpleName() + "\n"); 376 for (final int i : errors) 377 sb.append("Field \"" + names.get(i) + "\" " + msg + '\n'); 378 379 return sb.toString(); 380 } 381 382 383 // ******************************************************************************************** 384 // ******************************************************************************************** 385 // More Method(s) 386 // ******************************************************************************************** 387 // ******************************************************************************************** 388 389 390 // This seemingly pointless method is used by "ToString". 391 // 392 // Can be eliminated if I ever : 393 // * switch to JDK 21+ which use records and an auto-generated tostring 394 // * write my "To String Generator" classes. 395 // 396 // Right now, I am settling with "BT$ToString," which happens to need this method... 397 // Note that the "ugliness" of this method is that there is a static method in each and every 398 // subclass or descendant of "BaseType" that also has a method named "descriptor()" which 399 // happens to do the exact same thing as this one... 400 401 NestedDescriptor<DOMAIN_NESTED> getDescriptor() 402 { return helperSingleton.descriptor(); } 403 404 /** 405 * Accessor method for the private, internal {@code 'isPresent'} instance field. This 406 * field cannot be assigned inside of this abstract ancestor class' constructor. Thus, the 407 * {@code 'isPresent'} field also cannot be declared {@code 'final'}. As a result, the only 408 * other option for actually using or retrieving the values in this list is to include this 409 * simple {@code 'getter'} or {@code 'accessor'} method. 410 * 411 * <BR /><BR />This getter shields the internal, non-final field value from modification. 412 * 413 * @return The list contained by the {@code 'isPresent'} instace field. 414 */ 415 public final ReadOnlyList<Boolean> isPresent() 416 { return this.isPresent; } 417 418 /** 419 * Retrieves a {@link ReadOnlyMap} mapping field names to a {@link ReadOnlyList} of valid 420 * {@code String} values for each field in the map. 421 * 422 * <EMBED CLASS='external-html' DATA-FILE-ID=BaseType.allEnumStrROLs> 423 * @see NestedHelper#allEnumStrROLs() 424 * @see #enumStrValidate() 425 * @see #enumStrValidateThrow() 426 */ 427 public ReadOnlyMap<String, ReadOnlyList<String>> allEnumStrROLs() 428 { return helperSingleton.allEnumStrROLs(); } 429 430 /** 431 * Retrieve any associated string-enumeration lists for a given field, if they exist. 432 * 433 * @param fieldName The name of any field in this class, as a {@code java.lang.String} 434 * 435 * @return A read-only list of strings containing the string-enumeration that restricts the 436 * value of the field / property indicated by {@code 'fieldName'} 437 * 438 * <BR /><BR /> 439 * This method shall return <B><CODE>'null'</CODE></B> if: 440 * 441 * <BR /><BR /><UL CLASS=JDUL> 442 * 443 * <LI>The indicated field is not a {@code String} field or property in the first place</LI> 444 * 445 * <LI> The field is a {@code String} property, but isn't restricted by an string-enumeration 446 * lists, as per the CDP specification-definition files. 447 * </LI> 448 * </UL> 449 * 450 * @throws UnknownPropertyException If the {@code Class} which defines {@code 'this'} instance 451 * does not have the specified field in its class definition. 452 * 453 * @see #allEnumStrROLs 454 * @see NestedHelper#allEnumStrROLs() 455 */ 456 public ReadOnlyList<String> enumStrList(final String fieldName) 457 { 458 // If this class doesn't have a field named 'fieldName', then throw. 459 if (! helperSingleton.descriptor().names.contains(fieldName)) 460 throw new UnknownPropertyException(fieldName); 461 462 // Returns the list if it exists, and null if this field isn't an enum-restricted field 463 return helperSingleton.allEnumStrROLs().get(fieldName); 464 } 465}