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