View Javadoc
1   /*
2    * Copyright (c) 2002-2025 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 scopeConfiguration_;
57  
58      /**
59       * Constructor.
60       * @param browser the browser version to use
61       * @param scopeClass the scope class for faster access
62       */
63      protected AbstractJavaScriptConfiguration(final BrowserVersion browser, final Class<?> scopeClass) {
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 == scopeClass) {
71                      scopeConfiguration_ = 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) {
215                         final JsxGetter jsxGetter = (JsxGetter) annotation;
216                         if (isSupported(jsxGetter.value(), expectedBrowser)) {
217                             String property;
218                             if (jsxGetter.propertyName().isEmpty()) {
219                                 final int prefix = method.getName().startsWith("is") ? 2 : 3;
220                                 property = method.getName().substring(prefix);
221                                 property = Character.toLowerCase(property.charAt(0)) + property.substring(1);
222                             }
223                             else {
224                                 property = jsxGetter.propertyName();
225                             }
226                             allGetters.put(property, method);
227                         }
228                     }
229                     else if (annotation instanceof JsxSetter) {
230                         final JsxSetter jsxSetter = (JsxSetter) annotation;
231                         if (isSupported(jsxSetter.value(), expectedBrowser)) {
232                             String property;
233                             if (jsxSetter.propertyName().isEmpty()) {
234                                 property = method.getName().substring(3);
235                                 property = Character.toLowerCase(property.charAt(0)) + property.substring(1);
236                             }
237                             else {
238                                 property = jsxSetter.propertyName();
239                             }
240                             allSetters.put(property, method);
241                         }
242                     }
243                     if (annotation instanceof JsxSymbol) {
244                         final JsxSymbol jsxSymbol = (JsxSymbol) annotation;
245                         if (isSupported(jsxSymbol.value(), expectedBrowser)) {
246                             final String symbolKeyName;
247                             if (jsxSymbol.symbolName().isEmpty()) {
248                                 symbolKeyName = method.getName();
249                             }
250                             else {
251                                 symbolKeyName = jsxSymbol.symbolName();
252                             }
253 
254                             final SymbolKey symbolKey;
255                             if ("iterator".equalsIgnoreCase(symbolKeyName)) {
256                                 symbolKey = SymbolKey.ITERATOR;
257                             }
258                             else {
259                                 throw new RuntimeException("Invalid JsxSymbol annotation; unsupported '"
260                                         + symbolKeyName + "' symbol name.");
261                             }
262                             classConfiguration.addSymbol(symbolKey, method);
263                         }
264                     }
265                     else if (annotation instanceof JsxFunction) {
266                         final JsxFunction jsxFunction = (JsxFunction) annotation;
267                         if (isSupported(jsxFunction.value(), expectedBrowser)) {
268                             final String name;
269                             if (jsxFunction.functionName().isEmpty()) {
270                                 name = method.getName();
271                             }
272                             else {
273                                 name = jsxFunction.functionName();
274                             }
275                             classConfiguration.addFunction(name, method);
276                         }
277                     }
278                     else if (annotation instanceof JsxStaticGetter) {
279                         final JsxStaticGetter jsxStaticGetter = (JsxStaticGetter) annotation;
280                         if (isSupported(jsxStaticGetter.value(), expectedBrowser)) {
281                             final int prefix = method.getName().startsWith("is") ? 2 : 3;
282                             String property = method.getName().substring(prefix);
283                             property = Character.toLowerCase(property.charAt(0)) + property.substring(1);
284                             classConfiguration.addStaticProperty(property, method, null);
285                         }
286                     }
287                     else if (annotation instanceof JsxStaticFunction) {
288                         final JsxStaticFunction jsxStaticFunction = (JsxStaticFunction) annotation;
289                         if (isSupported(jsxStaticFunction.value(), expectedBrowser)) {
290                             final String name;
291                             if (jsxStaticFunction.functionName().isEmpty()) {
292                                 name = method.getName();
293                             }
294                             else {
295                                 name = jsxStaticFunction.functionName();
296                             }
297                             classConfiguration.addStaticFunction(name, method);
298                         }
299                     }
300                     else if (annotation instanceof JsxConstructor) {
301                         final JsxConstructor jsxConstructor = (JsxConstructor) annotation;
302                         if (isSupported(jsxConstructor.value(), expectedBrowser)) {
303                             final String name;
304                             if (jsxConstructor.functionName().isEmpty()) {
305                                 name = classConfiguration.getClassName();
306                             }
307                             else {
308                                 name = jsxConstructor.functionName();
309                             }
310                             classConfiguration.setJSConstructor(name, method);
311                         }
312                     }
313                     else if (annotation instanceof JsxConstructorAlias) {
314                         final JsxConstructorAlias jsxConstructorAlias = (JsxConstructorAlias) annotation;
315                         if (isSupported(jsxConstructorAlias.value(), expectedBrowser)) {
316                             classConfiguration.setJSConstructorAlias(jsxConstructorAlias.alias());
317                         }
318                     }
319                 }
320             }
321 
322             for (final Entry<String, Method> getterEntry : allGetters.entrySet()) {
323                 final String property = getterEntry.getKey();
324                 classConfiguration.addProperty(property, getterEntry.getValue(), allSetters.get(property));
325             }
326 
327             // JsxConstant/JsxSymbolConstant
328             for (final Field field : classConfiguration.getHostClass().getDeclaredFields()) {
329                 for (final Annotation annotation : field.getAnnotations()) {
330                     if (annotation instanceof JsxConstant) {
331                         final JsxConstant jsxConstant = (JsxConstant) annotation;
332                         if (isSupported(jsxConstant.value(), expectedBrowser)) {
333                             try {
334                                 classConfiguration.addConstant(field.getName(), field.get(null));
335                             }
336                             catch (final IllegalAccessException e) {
337                                 throw JavaScriptEngine.reportRuntimeError(
338                                         "Cannot get field '" + field.getName()
339                                         + "' for type: " + classConfiguration.getHostClass().getName()
340                                         + "reason: " + e.getMessage());
341                             }
342                         }
343                     }
344                     if (annotation instanceof JsxSymbolConstant) {
345                         final JsxSymbolConstant jsxSymbolConstant = (JsxSymbolConstant) annotation;
346                         if (isSupported(jsxSymbolConstant.value(), expectedBrowser)) {
347                             final SymbolKey symbolKey;
348                             if (StringUtils.startsWithIgnoreCase(field.getName(), "TO_STRING_TAG")) {
349                                 symbolKey = SymbolKey.TO_STRING_TAG;
350                             }
351                             else {
352                                 throw new RuntimeException("Invalid JsxSymbol annotation; unsupported '"
353                                         + field.getName() + "' symbol name.");
354                             }
355                             classConfiguration.addSymbolConstant(symbolKey, field.get(null).toString());
356                         }
357                     }
358                 }
359             }
360         }
361         catch (final Throwable e) {
362             throw new RuntimeException(
363                     "Processing classConfiguration for class "
364                             + classConfiguration.getHostClassSimpleName() + "failed."
365                             + " Reason: " + e, e);
366         }
367     }
368 
369     private static boolean isSupported(final SupportedBrowser[] browsers, final SupportedBrowser expectedBrowser) {
370         for (final SupportedBrowser browser : browsers) {
371             if (browser == expectedBrowser) {
372                 return true;
373             }
374         }
375         return false;
376     }
377 
378     /**
379      * Returns an immutable map containing the DOM to JavaScript mappings. Keys are
380      * java classes for the various DOM classes (e.g. HtmlInput.class) and the values
381      * are the JavaScript class names (e.g. "HTMLAnchorElement").
382      * @param clazz the class to get the scriptable for
383      * @return the mappings
384      */
385     public Class<? extends HtmlUnitScriptable> getDomJavaScriptMappingFor(final Class<?> clazz) {
386         if (domJavaScriptMap_ == null) {
387             final Map<Class<?>, Class<? extends HtmlUnitScriptable>> map =
388                     new ConcurrentHashMap<>(configuration_.size());
389 
390             final boolean debug = LOG.isDebugEnabled();
391             for (final ClassConfiguration classConfig : configuration_) {
392                 for (final Class<?> domClass : classConfig.getDomClasses()) {
393                     // preload and validate that the class exists
394                     if (debug) {
395                         LOG.debug("Mapping " + domClass.getName() + " to " + classConfig.getClassName());
396                     }
397                     map.put(domClass, classConfig.getHostClass());
398                 }
399             }
400 
401             domJavaScriptMap_ = map;
402         }
403 
404         return domJavaScriptMap_.get(clazz);
405     }
406 
407     protected ClassConfiguration getScopeConfiguration() {
408         return scopeConfiguration_;
409     }
410 }