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 &amp; 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 &amp; 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 &amp; types in the Browser &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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}