View Javadoc
1   /*
2    * Copyright (c) 2002-2026 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.javascript.configuration;
16  
17  import static org.htmlunit.javascript.configuration.SupportedBrowser.CHROME;
18  import static org.htmlunit.javascript.configuration.SupportedBrowser.EDGE;
19  import static org.htmlunit.javascript.configuration.SupportedBrowser.FF;
20  import static org.htmlunit.javascript.configuration.SupportedBrowser.FF_ESR;
21  
22  import java.lang.annotation.Annotation;
23  import java.lang.reflect.Field;
24  import java.lang.reflect.Method;
25  import java.util.ArrayList;
26  import java.util.HashSet;
27  import java.util.Map;
28  import java.util.Map.Entry;
29  import java.util.Set;
30  import java.util.concurrent.ConcurrentHashMap;
31  
32  import org.apache.commons.logging.Log;
33  import org.apache.commons.logging.LogFactory;
34  import org.htmlunit.BrowserVersion;
35  import org.htmlunit.corejs.javascript.SymbolKey;
36  import org.htmlunit.javascript.HtmlUnitScriptable;
37  import org.htmlunit.javascript.JavaScriptEngine;
38  import org.htmlunit.util.StringUtils;
39  
40  /**
41   * An abstract container for all the JavaScript configuration information.
42   *
43   * @author Mike Bowler
44   * @author Chris Erskine
45   * @author Ahmed Ashour
46   * @author Ronald Brill
47   * @author Frank Danek
48   */
49  public abstract class AbstractJavaScriptConfiguration {
50  
51      private static final Log LOG = LogFactory.getLog(AbstractJavaScriptConfiguration.class);
52  
53      private Map<Class<?>, Class<? extends HtmlUnitScriptable>> domJavaScriptMap_;
54  
55      private final ArrayList<ClassConfiguration> configuration_;
56      private ClassConfiguration globalThisConfiguration_;
57  
58      /**
59       * Constructor.
60       * @param browser the browser version to use
61       * @param globalThisClass the globalThis class for faster access
62       */
63      protected AbstractJavaScriptConfiguration(final BrowserVersion browser, final Class<?> globalThisClass) {
64          configuration_ = new ArrayList<>(getClasses().length);
65  
66          for (final Class<? extends HtmlUnitScriptable> klass : getClasses()) {
67              final ClassConfiguration config = getClassConfiguration(klass, browser);
68              if (config != null) {
69                  configuration_.add(config);
70                  if (klass == globalThisClass) {
71                      globalThisConfiguration_ = config;
72                  }
73              }
74          }
75      }
76  
77      /**
78       * @return the classes configured by this configuration
79       */
80      protected abstract Class<? extends HtmlUnitScriptable>[] getClasses();
81  
82      /**
83       * Gets all the configurations.
84       * @return the class configurations
85       */
86      public Iterable<ClassConfiguration> getAll() {
87          return configuration_;
88      }
89  
90      /**
91       * Returns the class configuration of the given {@code klass}.
92       *
93       * @param klass the class
94       * @param browserVersion the browser version
95       * @return the class configuration
96       */
97      public static ClassConfiguration getClassConfiguration(final Class<? extends HtmlUnitScriptable> klass,
98          final BrowserVersion browserVersion) {
99          if (browserVersion != null) {
100             final SupportedBrowser expectedBrowser;
101             if (browserVersion.isChrome()) {
102                 expectedBrowser = CHROME;
103             }
104             else if (browserVersion.isEdge()) {
105                 expectedBrowser = EDGE;
106             }
107             else if (browserVersion.isFirefoxESR()) {
108                 expectedBrowser = FF_ESR;
109             }
110             else if (browserVersion.isFirefox()) {
111                 expectedBrowser = FF;
112             }
113             else {
114                 expectedBrowser = CHROME;  // our current fallback
115             }
116 
117             final String hostClassName = klass.getName();
118             final JsxClasses jsxClasses = klass.getAnnotation(JsxClasses.class);
119             if (jsxClasses != null) {
120                 if (klass.getAnnotation(JsxClass.class) != null) {
121                     throw new RuntimeException("Invalid JsxClasses/JsxClass annotation; class '"
122                         + hostClassName + "' has both.");
123                 }
124                 final JsxClass[] jsxClassValues = jsxClasses.value();
125                 if (jsxClassValues.length == 1) {
126                     throw new RuntimeException("No need to specify JsxClasses with a single JsxClass for "
127                             + hostClassName);
128                 }
129                 final Set<Class<?>> domClasses = new HashSet<>();
130 
131                 boolean isJsObject = false;
132                 String className = null;
133 
134                 final String extendedClassName;
135                 final Class<?> superClass = klass.getSuperclass();
136                 if (superClass.getAnnotation(JsxClass.class) == null
137                         && superClass.getAnnotation(JsxClasses.class) == null) {
138                     extendedClassName = "";
139                 }
140                 else {
141                     extendedClassName = superClass.getSimpleName();
142                 }
143 
144                 for (final JsxClass jsxClass : jsxClassValues) {
145                     if (jsxClass != null && isSupported(jsxClass.value(), expectedBrowser)) {
146                         domClasses.add(jsxClass.domClass());
147                         if (jsxClass.isJSObject()) {
148                             isJsObject = true;
149                         }
150                         if (!jsxClass.className().isEmpty()) {
151                             className = jsxClass.className();
152                         }
153                     }
154                 }
155 
156                 if (domClasses.size() > 0) {
157                     final ClassConfiguration classConfiguration =
158                             new ClassConfiguration(klass, domClasses.toArray(new Class<?>[0]), isJsObject,
159                                     className, extendedClassName);
160 
161                     process(classConfiguration, expectedBrowser);
162                     return classConfiguration;
163                 }
164             }
165 
166             final JsxClass jsxClass = klass.getAnnotation(JsxClass.class);
167             if (jsxClass != null && isSupported(jsxClass.value(), expectedBrowser)) {
168 
169                 final Set<Class<?>> domClasses = new HashSet<>();
170                 final Class<?> domClass = jsxClass.domClass();
171                 if (domClass != null && domClass != Object.class) {
172                     domClasses.add(domClass);
173                 }
174 
175                 String className = jsxClass.className();
176                 if (className.isEmpty()) {
177                     className = null;
178                 }
179 
180                 final String extendedClassName;
181                 final Class<?> superClass = klass.getSuperclass();
182                 if (superClass.getAnnotation(JsxClass.class) == null
183                         && superClass.getAnnotation(JsxClasses.class) == null) {
184                     extendedClassName = "";
185                 }
186                 else {
187                     extendedClassName = superClass.getSimpleName();
188                 }
189 
190                 final ClassConfiguration classConfiguration
191                     = new ClassConfiguration(klass,
192                             domClasses.toArray(new Class<?>[0]),
193                             jsxClass.isJSObject(),
194                             className,
195                             extendedClassName);
196 
197                 process(classConfiguration, expectedBrowser);
198                 return classConfiguration;
199             }
200         }
201         return null;
202     }
203 
204     private static void process(final ClassConfiguration classConfiguration, final SupportedBrowser expectedBrowser) {
205         final Map<String, Method> allGetters = new ConcurrentHashMap<>();
206         final Map<String, Method> allSetters = new ConcurrentHashMap<>();
207 
208         try {
209             // do this as first step to be able to overwrite the symbol later if needed
210             classConfiguration.addSymbolConstant(SymbolKey.TO_STRING_TAG, classConfiguration.getHostClassSimpleName());
211 
212             for (final Method method : classConfiguration.getHostClass().getDeclaredMethods()) {
213                 for (final Annotation annotation : method.getAnnotations()) {
214                     if (annotation instanceof JsxGetter jsxGetter) {
215                         if (isSupported(jsxGetter.value(), expectedBrowser)) {
216                             String property;
217                             if (jsxGetter.propertyName().isEmpty()) {
218                                 final int prefix = method.getName().startsWith("is") ? 2 : 3;
219                                 property = method.getName().substring(prefix);
220                                 property = Character.toLowerCase(property.charAt(0)) + property.substring(1);
221                             }
222                             else {
223                                 property = jsxGetter.propertyName();
224                             }
225                             allGetters.put(property, method);
226                         }
227                     }
228                     else if (annotation instanceof JsxSetter jsxSetter) {
229                         if (isSupported(jsxSetter.value(), expectedBrowser)) {
230                             String property;
231                             if (jsxSetter.propertyName().isEmpty()) {
232                                 property = method.getName().substring(3);
233                                 property = Character.toLowerCase(property.charAt(0)) + property.substring(1);
234                             }
235                             else {
236                                 property = jsxSetter.propertyName();
237                             }
238                             allSetters.put(property, method);
239                         }
240                     }
241                     if (annotation instanceof JsxSymbol jsxSymbol) {
242                         if (isSupported(jsxSymbol.value(), expectedBrowser)) {
243                             final String symbolKeyName;
244                             if (jsxSymbol.symbolName().isEmpty()) {
245                                 symbolKeyName = method.getName();
246                             }
247                             else {
248                                 symbolKeyName = jsxSymbol.symbolName();
249                             }
250 
251                             final SymbolKey symbolKey;
252                             if ("iterator".equalsIgnoreCase(symbolKeyName)) {
253                                 symbolKey = SymbolKey.ITERATOR;
254                             }
255                             else {
256                                 throw new RuntimeException("Invalid JsxSymbol annotation; unsupported '"
257                                         + symbolKeyName + "' symbol name.");
258                             }
259                             classConfiguration.addSymbol(symbolKey, method);
260                         }
261                     }
262                     else if (annotation instanceof JsxFunction jsxFunction) {
263                         if (isSupported(jsxFunction.value(), expectedBrowser)) {
264                             final String name;
265                             if (jsxFunction.functionName().isEmpty()) {
266                                 name = method.getName();
267                             }
268                             else {
269                                 name = jsxFunction.functionName();
270                             }
271                             classConfiguration.addFunction(name, method);
272                         }
273                     }
274                     else if (annotation instanceof JsxStaticGetter jsxStaticGetter) {
275                         if (isSupported(jsxStaticGetter.value(), expectedBrowser)) {
276                             final int prefix = method.getName().startsWith("is") ? 2 : 3;
277                             String property = method.getName().substring(prefix);
278                             property = Character.toLowerCase(property.charAt(0)) + property.substring(1);
279                             classConfiguration.addStaticProperty(property, method, null);
280                         }
281                     }
282                     else if (annotation instanceof JsxStaticFunction jsxStaticFunction) {
283                         if (isSupported(jsxStaticFunction.value(), expectedBrowser)) {
284                             final String name;
285                             if (jsxStaticFunction.functionName().isEmpty()) {
286                                 name = method.getName();
287                             }
288                             else {
289                                 name = jsxStaticFunction.functionName();
290                             }
291                             classConfiguration.addStaticFunction(name, method);
292                         }
293                     }
294                     else if (annotation instanceof JsxConstructor jsxConstructor) {
295                         if (isSupported(jsxConstructor.value(), expectedBrowser)) {
296                             final String name;
297                             if (jsxConstructor.functionName().isEmpty()) {
298                                 name = classConfiguration.getClassName();
299                             }
300                             else {
301                                 name = jsxConstructor.functionName();
302                             }
303                             classConfiguration.setJSConstructor(name, method);
304                         }
305                     }
306                     else if (annotation instanceof JsxConstructorAlias jsxConstructorAlias) {
307                         if (isSupported(jsxConstructorAlias.value(), expectedBrowser)) {
308                             classConfiguration.setJSConstructorAlias(jsxConstructorAlias.alias());
309                         }
310                     }
311                 }
312             }
313 
314             for (final Entry<String, Method> getterEntry : allGetters.entrySet()) {
315                 final String property = getterEntry.getKey();
316                 classConfiguration.addProperty(property, getterEntry.getValue(), allSetters.get(property));
317             }
318 
319             // JsxConstant/JsxSymbolConstant
320             for (final Field field : classConfiguration.getHostClass().getDeclaredFields()) {
321                 for (final Annotation annotation : field.getAnnotations()) {
322                     if (annotation instanceof JsxConstant jsxConstant) {
323                         if (isSupported(jsxConstant.value(), expectedBrowser)) {
324                             try {
325                                 classConfiguration.addConstant(field.getName(), field.get(null));
326                             }
327                             catch (final IllegalAccessException e) {
328                                 throw JavaScriptEngine.reportRuntimeError(
329                                         "Cannot get field '" + field.getName()
330                                         + "' for type: " + classConfiguration.getHostClass().getName()
331                                         + "reason: " + e.getMessage());
332                             }
333                         }
334                     }
335                     if (annotation instanceof JsxSymbolConstant jsxSymbolConstant) {
336                         if (isSupported(jsxSymbolConstant.value(), expectedBrowser)) {
337                             final SymbolKey symbolKey;
338                             if (StringUtils.startsWithIgnoreCase(field.getName(), "TO_STRING_TAG")) {
339                                 symbolKey = SymbolKey.TO_STRING_TAG;
340                             }
341                             else {
342                                 throw new RuntimeException("Invalid JsxSymbol annotation; unsupported '"
343                                         + field.getName() + "' symbol name.");
344                             }
345                             classConfiguration.addSymbolConstant(symbolKey, field.get(null).toString());
346                         }
347                     }
348                 }
349             }
350         }
351         catch (final Throwable e) {
352             throw new RuntimeException(
353                     "Processing classConfiguration for class "
354                             + classConfiguration.getHostClassSimpleName() + "failed."
355                             + " Reason: " + e, e);
356         }
357     }
358 
359     private static boolean isSupported(final SupportedBrowser[] browsers, final SupportedBrowser expectedBrowser) {
360         for (final SupportedBrowser browser : browsers) {
361             if (browser == expectedBrowser) {
362                 return true;
363             }
364         }
365         return false;
366     }
367 
368     /**
369      * Returns an immutable map containing the DOM to JavaScript mappings. Keys are
370      * java classes for the various DOM classes (e.g. HtmlInput.class) and the values
371      * are the JavaScript class names (e.g. "HTMLAnchorElement").
372      * @param clazz the class to get the scriptable for
373      * @return the mappings
374      */
375     public Class<? extends HtmlUnitScriptable> getDomJavaScriptMappingFor(final Class<?> clazz) {
376         if (domJavaScriptMap_ == null) {
377             final Map<Class<?>, Class<? extends HtmlUnitScriptable>> map =
378                     new ConcurrentHashMap<>(configuration_.size());
379 
380             final boolean debug = LOG.isDebugEnabled();
381             for (final ClassConfiguration classConfig : configuration_) {
382                 for (final Class<?> domClass : classConfig.getDomClasses()) {
383                     // preload and validate that the class exists
384                     if (debug) {
385                         LOG.debug("Mapping " + domClass.getName() + " to " + classConfig.getClassName());
386                     }
387                     map.put(domClass, classConfig.getHostClass());
388                 }
389             }
390 
391             domJavaScriptMap_ = map;
392         }
393 
394         return domJavaScriptMap_.get(clazz);
395     }
396 
397     protected ClassConfiguration getGlobalThisConfiguration() {
398         return globalThisConfiguration_;
399     }
400 }