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  
39  /**
40   * An abstract container for all the JavaScript configuration information.
41   *
42   * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
43   * @author Chris Erskine
44   * @author Ahmed Ashour
45   * @author Ronald Brill
46   * @author Frank Danek
47   */
48  public abstract class AbstractJavaScriptConfiguration {
49  
50      private static final Log LOG = LogFactory.getLog(AbstractJavaScriptConfiguration.class);
51  
52      private Map<Class<?>, Class<? extends HtmlUnitScriptable>> domJavaScriptMap_;
53  
54      private final ArrayList<ClassConfiguration> configuration_;
55      private ClassConfiguration scopeConfiguration_;
56  
57      /**
58       * Constructor.
59       * @param browser the browser version to use
60       * @param scopeClass the scope class for faster access
61       */
62      protected AbstractJavaScriptConfiguration(final BrowserVersion browser, final Class<?> scopeClass) {
63          configuration_ = new ArrayList<>(getClasses().length);
64  
65          for (final Class<? extends HtmlUnitScriptable> klass : getClasses()) {
66              final ClassConfiguration config = getClassConfiguration(klass, browser);
67              if (config != null) {
68                  configuration_.add(config);
69                  if (klass == scopeClass) {
70                      scopeConfiguration_ = config;
71                  }
72              }
73          }
74      }
75  
76      /**
77       * @return the classes configured by this configuration
78       */
79      protected abstract Class<? extends HtmlUnitScriptable>[] getClasses();
80  
81      /**
82       * Gets all the configurations.
83       * @return the class configurations
84       */
85      public Iterable<ClassConfiguration> getAll() {
86          return configuration_;
87      }
88  
89      /**
90       * Returns the class configuration of the given {@code klass}.
91       *
92       * @param klass the class
93       * @param browserVersion the browser version
94       * @return the class configuration
95       */
96      public static ClassConfiguration getClassConfiguration(final Class<? extends HtmlUnitScriptable> klass,
97          final BrowserVersion browserVersion) {
98          if (browserVersion != null) {
99              final SupportedBrowser expectedBrowser;
100             if (browserVersion.isChrome()) {
101                 expectedBrowser = CHROME;
102             }
103             else if (browserVersion.isEdge()) {
104                 expectedBrowser = EDGE;
105             }
106             else if (browserVersion.isFirefoxESR()) {
107                 expectedBrowser = FF_ESR;
108             }
109             else if (browserVersion.isFirefox()) {
110                 expectedBrowser = FF;
111             }
112             else {
113                 expectedBrowser = CHROME;  // our current fallback
114             }
115 
116             final String hostClassName = klass.getName();
117             final JsxClasses jsxClasses = klass.getAnnotation(JsxClasses.class);
118             if (jsxClasses != null) {
119                 if (klass.getAnnotation(JsxClass.class) != null) {
120                     throw new RuntimeException("Invalid JsxClasses/JsxClass annotation; class '"
121                         + hostClassName + "' has both.");
122                 }
123                 final JsxClass[] jsxClassValues = jsxClasses.value();
124                 if (jsxClassValues.length == 1) {
125                     throw new RuntimeException("No need to specify JsxClasses with a single JsxClass for "
126                             + hostClassName);
127                 }
128                 final Set<Class<?>> domClasses = new HashSet<>();
129 
130                 boolean isJsObject = false;
131                 String className = null;
132 
133                 final String extendedClassName;
134                 final Class<?> superClass = klass.getSuperclass();
135                 if (superClass.getAnnotation(JsxClass.class) == null
136                         && superClass.getAnnotation(JsxClasses.class) == null) {
137                     extendedClassName = "";
138                 }
139                 else {
140                     extendedClassName = superClass.getSimpleName();
141                 }
142 
143                 for (final JsxClass jsxClass : jsxClassValues) {
144                     if (jsxClass != null && isSupported(jsxClass.value(), expectedBrowser)) {
145                         domClasses.add(jsxClass.domClass());
146                         if (jsxClass.isJSObject()) {
147                             isJsObject = true;
148                         }
149                         if (!jsxClass.className().isEmpty()) {
150                             className = jsxClass.className();
151                         }
152                     }
153                 }
154 
155                 final ClassConfiguration classConfiguration =
156                         new ClassConfiguration(klass, domClasses.toArray(new Class<?>[0]), isJsObject,
157                                 className, extendedClassName);
158 
159                 process(classConfiguration, expectedBrowser);
160                 return classConfiguration;
161             }
162 
163             final JsxClass jsxClass = klass.getAnnotation(JsxClass.class);
164             if (jsxClass != null && isSupported(jsxClass.value(), expectedBrowser)) {
165 
166                 final Set<Class<?>> domClasses = new HashSet<>();
167                 final Class<?> domClass = jsxClass.domClass();
168                 if (domClass != null && domClass != Object.class) {
169                     domClasses.add(domClass);
170                 }
171 
172                 String className = jsxClass.className();
173                 if (className.isEmpty()) {
174                     className = null;
175                 }
176 
177                 final String extendedClassName;
178                 final Class<?> superClass = klass.getSuperclass();
179                 if (superClass.getAnnotation(JsxClass.class) == null
180                         && superClass.getAnnotation(JsxClasses.class) == null) {
181                     extendedClassName = "";
182                 }
183                 else {
184                     extendedClassName = superClass.getSimpleName();
185                 }
186 
187                 final ClassConfiguration classConfiguration
188                     = new ClassConfiguration(klass,
189                             domClasses.toArray(new Class<?>[0]),
190                             jsxClass.isJSObject(),
191                             className,
192                             extendedClassName);
193 
194                 process(classConfiguration, expectedBrowser);
195                 return classConfiguration;
196             }
197         }
198         return null;
199     }
200 
201     private static void process(final ClassConfiguration classConfiguration, final SupportedBrowser expectedBrowser) {
202         final Map<String, Method> allGetters = new ConcurrentHashMap<>();
203         final Map<String, Method> allSetters = new ConcurrentHashMap<>();
204 
205         try {
206             // do this as first step to be able to overwrite the symbol later if needed
207             classConfiguration.addSymbolConstant(SymbolKey.TO_STRING_TAG, classConfiguration.getHostClassSimpleName());
208 
209             for (final Method method : classConfiguration.getHostClass().getDeclaredMethods()) {
210                 for (final Annotation annotation : method.getAnnotations()) {
211                     if (annotation instanceof JsxGetter) {
212                         final JsxGetter jsxGetter = (JsxGetter) annotation;
213                         if (isSupported(jsxGetter.value(), expectedBrowser)) {
214                             String property;
215                             if (jsxGetter.propertyName().isEmpty()) {
216                                 final int prefix = method.getName().startsWith("is") ? 2 : 3;
217                                 property = method.getName().substring(prefix);
218                                 property = Character.toLowerCase(property.charAt(0)) + property.substring(1);
219                             }
220                             else {
221                                 property = jsxGetter.propertyName();
222                             }
223                             allGetters.put(property, method);
224                         }
225                     }
226                     else if (annotation instanceof JsxSetter) {
227                         final JsxSetter jsxSetter = (JsxSetter) annotation;
228                         if (isSupported(jsxSetter.value(), expectedBrowser)) {
229                             String property;
230                             if (jsxSetter.propertyName().isEmpty()) {
231                                 property = method.getName().substring(3);
232                                 property = Character.toLowerCase(property.charAt(0)) + property.substring(1);
233                             }
234                             else {
235                                 property = jsxSetter.propertyName();
236                             }
237                             allSetters.put(property, method);
238                         }
239                     }
240                     if (annotation instanceof JsxSymbol) {
241                         final JsxSymbol jsxSymbol = (JsxSymbol) annotation;
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) {
263                         final JsxFunction jsxFunction = (JsxFunction) annotation;
264                         if (isSupported(jsxFunction.value(), expectedBrowser)) {
265                             final String name;
266                             if (jsxFunction.functionName().isEmpty()) {
267                                 name = method.getName();
268                             }
269                             else {
270                                 name = jsxFunction.functionName();
271                             }
272                             classConfiguration.addFunction(name, method);
273                         }
274                     }
275                     else if (annotation instanceof JsxStaticGetter) {
276                         final JsxStaticGetter jsxStaticGetter = (JsxStaticGetter) annotation;
277                         if (isSupported(jsxStaticGetter.value(), expectedBrowser)) {
278                             final int prefix = method.getName().startsWith("is") ? 2 : 3;
279                             String property = method.getName().substring(prefix);
280                             property = Character.toLowerCase(property.charAt(0)) + property.substring(1);
281                             classConfiguration.addStaticProperty(property, method, null);
282                         }
283                     }
284                     else if (annotation instanceof JsxStaticFunction) {
285                         final JsxStaticFunction jsxStaticFunction = (JsxStaticFunction) annotation;
286                         if (isSupported(jsxStaticFunction.value(), expectedBrowser)) {
287                             final String name;
288                             if (jsxStaticFunction.functionName().isEmpty()) {
289                                 name = method.getName();
290                             }
291                             else {
292                                 name = jsxStaticFunction.functionName();
293                             }
294                             classConfiguration.addStaticFunction(name, method);
295                         }
296                     }
297                     else if (annotation instanceof JsxConstructor) {
298                         final JsxConstructor jsxConstructor = (JsxConstructor) annotation;
299                         if (isSupported(jsxConstructor.value(), expectedBrowser)) {
300                             final String name;
301                             if (jsxConstructor.functionName().isEmpty()) {
302                                 name = classConfiguration.getClassName();
303                             }
304                             else {
305                                 name = jsxConstructor.functionName();
306                             }
307                             classConfiguration.setJSConstructor(name, method);
308                         }
309                     }
310                     else if (annotation instanceof JsxConstructorAlias) {
311                         final JsxConstructorAlias jsxConstructorAlias = (JsxConstructorAlias) annotation;
312                         if (isSupported(jsxConstructorAlias.value(), expectedBrowser)) {
313                             classConfiguration.setJSConstructorAlias(jsxConstructorAlias.alias());
314                         }
315                     }
316                 }
317             }
318 
319             for (final Entry<String, Method> getterEntry : allGetters.entrySet()) {
320                 final String property = getterEntry.getKey();
321                 classConfiguration.addProperty(property, getterEntry.getValue(), allSetters.get(property));
322             }
323 
324             // JsxConstant/JsxSymbolConstant
325             for (final Field field : classConfiguration.getHostClass().getDeclaredFields()) {
326                 for (final Annotation annotation : field.getAnnotations()) {
327                     if (annotation instanceof JsxConstant) {
328                         final JsxConstant jsxConstant = (JsxConstant) annotation;
329                         if (isSupported(jsxConstant.value(), expectedBrowser)) {
330                             try {
331                                 classConfiguration.addConstant(field.getName(), field.get(null));
332                             }
333                             catch (final IllegalAccessException e) {
334                                 throw JavaScriptEngine.reportRuntimeError(
335                                         "Cannot get field '" + field.getName()
336                                         + "' for type: " + classConfiguration.getHostClass().getName()
337                                         + "reason: " + e.getMessage());
338                             }
339                         }
340                     }
341                     if (annotation instanceof JsxSymbolConstant) {
342                         final JsxSymbolConstant jsxSymbolConstant = (JsxSymbolConstant) annotation;
343                         if (isSupported(jsxSymbolConstant.value(), expectedBrowser)) {
344                             final SymbolKey symbolKey;
345                             if ("TO_STRING_TAG".equalsIgnoreCase(field.getName())) {
346                                 symbolKey = SymbolKey.TO_STRING_TAG;
347                             }
348                             else {
349                                 throw new RuntimeException("Invalid JsxSymbol annotation; unsupported '"
350                                         + field.getName() + "' symbol name.");
351                             }
352                             classConfiguration.addSymbolConstant(symbolKey, field.get(null).toString());
353                         }
354                     }
355                 }
356             }
357         }
358         catch (final Throwable e) {
359             throw new RuntimeException(
360                     "Processing classConfiguration for class "
361                             + classConfiguration.getHostClassSimpleName() + "failed."
362                             + " Reason: " + e, e);
363         }
364     }
365 
366     private static boolean isSupported(final SupportedBrowser[] browsers, final SupportedBrowser expectedBrowser) {
367         for (final SupportedBrowser browser : browsers) {
368             if (browser == expectedBrowser) {
369                 return true;
370             }
371         }
372         return false;
373     }
374 
375     /**
376      * Returns whether the two {@link SupportedBrowser} are compatible or not.
377      * @param browser1 the first {@link SupportedBrowser}
378      * @param browser2 the second {@link SupportedBrowser}
379      * @return whether the two {@link SupportedBrowser} are compatible or not
380      *
381      * @deprecated as of version 4.8.0; will be removed without replacement
382      */
383     @Deprecated
384     public static boolean isCompatible(final SupportedBrowser browser1, final SupportedBrowser browser2) {
385         return browser1 == browser2;
386     }
387 
388     /**
389      * Returns an immutable map containing the DOM to JavaScript mappings. Keys are
390      * java classes for the various DOM classes (e.g. HtmlInput.class) and the values
391      * are the JavaScript class names (e.g. "HTMLAnchorElement").
392      * @param clazz the class to get the scriptable for
393      * @return the mappings
394      */
395     public Class<? extends HtmlUnitScriptable> getDomJavaScriptMappingFor(final Class<?> clazz) {
396         if (domJavaScriptMap_ == null) {
397             final Map<Class<?>, Class<? extends HtmlUnitScriptable>> map =
398                     new ConcurrentHashMap<>(configuration_.size());
399 
400             final boolean debug = LOG.isDebugEnabled();
401             for (final ClassConfiguration classConfig : configuration_) {
402                 for (final Class<?> domClass : classConfig.getDomClasses()) {
403                     // preload and validate that the class exists
404                     if (debug) {
405                         LOG.debug("Mapping " + domClass.getName() + " to " + classConfig.getClassName());
406                     }
407                     map.put(domClass, classConfig.getHostClass());
408                 }
409             }
410 
411             domJavaScriptMap_ = map;
412         }
413 
414         return domJavaScriptMap_.get(clazz);
415     }
416 
417     protected ClassConfiguration getScopeConfiguration() {
418         return scopeConfiguration_;
419     }
420 }