001package Torello.Browser;
002
003import static Torello.Java.StrPrint.I4;
004
005import java.lang.reflect.Method;
006import java.lang.reflect.Modifier;
007import java.lang.reflect.MalformedParametersException;
008
009import Torello.Java.ReadOnly.ReadOnlyArrayList;
010import Torello.Java.ReadOnly.ReadOnlyList;
011
012/**
013 * Reflected information / data which has been extracted from one of the generated CDP domain
014 * command methods.
015 * 
016 * <BR /><BR />
017 * A "Domain Command" is any static method generated inside one of the primary browser domain
018 * classes.  For example:
019 * 
020 * <BR /><BR />
021 * <B><CODE>{@link Torello.Browser.JavaScriptAPI.RunTime#evaluate}</CODE></B>
022 * 
023 * <BR /><BR />
024 * is a command method declared inside the {@code RunTime} domain class.  It accepts 16 input
025 * parameters, most of which are optional.  Its corresponding {@code CommandDescriptor} singleton
026 * is created and stored inside the generated command-helper class for that domain:
027 * 
028 * <BR /><BR />
029 * <B><CODE><A HREF='JavaScriptAPI/hilite-files/RunTime$$Commands.java.html'>RunTime$$Commands
030 * </A></CODE></B>
031 * 
032 * <BR /><BR />
033 * The descriptor stores the command name, browser domain, parameter names, CDP type constants,
034 * optional-parameter flags, experimental-parameter flags, and the Java {@link Class} returned by
035 * the command.  This metadata is used by {@link CommandBuilder} to construct a {@link Script}
036 * without requiring the user to write long method invocations filled with {@code null}
037 * placeholders.
038 * 
039 * @param <T> The Java type returned by the {@link Script} created for this command.  This type
040 * parameter allows {@code CommandDescriptor<T>} to work together with
041 * {@link CommandBuilder}{@code <T>}.
042 */
043public class CommandDescriptor<T>
044    extends AbstractDescriptor
045    implements java.io.Serializable
046{
047    // ********************************************************************************************
048    // ********************************************************************************************
049    // Fields
050    // ********************************************************************************************
051    // ********************************************************************************************
052
053
054    /** <EMBED CLASS='external-html' DATA-FILE-ID=SVUID> */
055    protected static final long serialVersionUID = 1L;
056
057    /** The Java {@code Class} returned by the command. */
058    public final Class<T> returnClass;
059
060    // This package-private field is used by the "CommandBuilder" class's "build()" method
061    private transient java.lang.reflect.Method method = null;
062
063    // This is used below
064    private transient java.lang.reflect.Parameter[] parameters = null;
065
066
067    // ********************************************************************************************
068    // ********************************************************************************************
069    // Constructor & toString
070    // ********************************************************************************************
071    // ********************************************************************************************
072
073
074    /**
075     * This is a public constructor, but it's primarily intended for internal use.
076     * 
077     * <!-- NOTE: Chat-GPT wrote these "Single Line Summaries!"  Pretty nifty... -->
078     * 
079     * @param name The name of the CDP type described by this descriptor.
080     * @param domain The Browser Domain containing the described CDP type.
081     * @param names Parallel list of CDP property names.
082     * @param types Parallel list of {@link CDPTypes CDP Type Constants}.
083     * @param optionals Parallel list indicating which properties are {@code optional}.
084     * @param experimentals Parallel list indicating which properties are {@code experimental}.
085     * @param returnClass The Java {@code Class} returned by the command.
086     */
087    public CommandDescriptor(
088            final String                name,
089            final Domains               domain,
090            final ReadOnlyList<String>  names,
091            final ReadOnlyList<Byte>    types,
092            final ReadOnlyList<Boolean> optionals,
093            final ReadOnlyList<Boolean> experimentals,
094            final Class<T>              returnClass
095        )
096    {
097        super(name, domain, names, types, optionals, experimentals);
098        this.returnClass = returnClass;
099    }
100
101    /**
102     * Generates a reasonable {@code String} representation of {@code 'this'} instance.
103     * @return A Java {@code String}
104     */
105    @Override
106    public String toString()
107    {
108        return 
109            "CommandDescriptor:\n" +
110            "{\n" +
111            super.toString() +
112            '\n' +
113            I4 + "returnClass:   " + this.returnClass.getCanonicalName() + '\n' +
114            "}\n";
115    }
116
117
118    // ********************************************************************************************
119    // ********************************************************************************************
120    // Package-Private Reflection Methods
121    // ********************************************************************************************
122    // ********************************************************************************************
123
124
125    // Allows the CommandBuilder to verify if a user has passed a valid class
126    Class<?> getClass(final String name, final int index)
127    {
128        if (this.parameters == null)
129
130            try 
131                { this.parameters = this.method().getParameters(); }
132
133            catch (MalformedParametersException e)
134                { throw new CommandDescriptorError("Unable to retrieve method parameters", e); }
135
136        return this.parameters[index].getType();
137    }
138
139    Method method()
140    {
141        if (this.method != null) return method;
142
143        try
144        {
145            final Method[] methods =
146                this.domain.getDomainClass().getMethods();
147
148            if (methods == null)
149                throw new CommandDescriptorError("getMethods() returned null");
150
151            Method found = null;
152
153            for (final Method m : methods)
154            {
155                if (m == null) continue;
156
157                /*
158                System.out.println(
159                    "m.getName():       " + m.getName()                         + '\n' +
160                    "this.name:         " + this.name                           + '\n' +
161                    "m.getReturnType(): " + m.getReturnType()                   + '\n' +
162                    "this.returnClass:  " + this.returnClass.getCanonicalName() + '\n' +
163                    '\n'
164                );
165                */
166
167                if (m.getName().equals(this.name))
168                    if (m.getReturnType().equals(Script.class))
169                    {
170                        if (found != null) throw new CommandDescriptorError(
171                            "More than one method matched command name '" +
172                            this.name + '\''
173                        );
174
175                        found = m;
176                    }
177            }
178
179            if (found == null) throw new CommandDescriptorError
180                ("Unable to locate command method named '" + this.name + '\'');
181
182            if (Modifier.isStatic(found.getModifiers()) == false) throw new CommandDescriptorError
183                ("The reflected command method is not static.");
184
185            this.method = found;
186        }
187
188        catch (SecurityException e)
189        {
190            throw new CommandDescriptorError(
191                "There was an error extracting this command's method from its Domain Class.  " +
192                "Please see the getCause() throwable for details.",
193                e
194            );
195        }
196
197        if (method.getParameterCount() != this.size) throw new CommandDescriptorError(
198            "The number of parameters in the method which was retrieved is " +
199            method.getParameterCount() + ", but the number of parameters listed in the " +
200            "command descriptor is " + this.size + ".  These numbers should be equal."
201        );
202
203        return this.method;
204    }
205
206    private static class CommandDescriptorError extends Error
207    {
208        protected static final long serialVersionUID = 1;
209
210        private CommandDescriptorError(final String message)
211        { super(message); }
212
213        private CommandDescriptorError(final String message, final Throwable cause)
214        { super(message, cause); }
215    }
216}