1
2
3
4
5
6
7
8
9
10
11
12
13
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
42
43
44
45
46
47
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
60
61
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
79
80
81
82 protected abstract Class<? extends HtmlUnitScriptable>[] getClasses();
83
84
85
86
87
88 public Iterable<ClassConfiguration> getAll() {
89 return configuration_;
90 }
91
92
93
94
95
96
97
98
99 public static ClassConfiguration getClassConfiguration(final Class<? extends HtmlUnitScriptable> klass,
100 final BrowserVersion browserVersion) {
101 if (browserVersion != null) {
102 final SupportedBrowser expectedBrowser;
103 if (browserVersion.isChrome()) {
104 expectedBrowser = CHROME;
105 }
106 else if (browserVersion.isEdge()) {
107 expectedBrowser = EDGE;
108 }
109 else if (browserVersion.isFirefoxESR()) {
110 expectedBrowser = FF_ESR;
111 }
112 else if (browserVersion.isFirefox()) {
113 expectedBrowser = FF;
114 }
115 else {
116 expectedBrowser = CHROME;
117 }
118
119 final String hostClassName = klass.getName();
120 final JsxClasses jsxClasses = klass.getAnnotation(JsxClasses.class);
121 if (jsxClasses != null) {
122 if (klass.getAnnotation(JsxClass.class) != null) {
123 throw new RuntimeException("Invalid JsxClasses/JsxClass annotation; class '"
124 + hostClassName + "' has both.");
125 }
126 final JsxClass[] jsxClassValues = jsxClasses.value();
127 if (jsxClassValues.length == 1) {
128 throw new RuntimeException("No need to specify JsxClasses with a single JsxClass for "
129 + hostClassName);
130 }
131 final Set<Class<?>> domClasses = new HashSet<>();
132
133 boolean isJsObject = false;
134 String className = null;
135
136 final String extendedClassName;
137 final Class<?> superClass = klass.getSuperclass();
138 if (superClass.getAnnotation(JsxClass.class) == null
139 && superClass.getAnnotation(JsxClasses.class) == null) {
140 extendedClassName = "";
141 }
142 else {
143 extendedClassName = superClass.getSimpleName();
144 }
145
146 for (final JsxClass jsxClass : jsxClassValues) {
147 if (jsxClass != null && isSupported(jsxClass.value(), expectedBrowser)) {
148 domClasses.add(jsxClass.domClass());
149 if (jsxClass.isJSObject()) {
150 isJsObject = true;
151 }
152 if (!jsxClass.className().isEmpty()) {
153 className = jsxClass.className();
154 }
155 }
156 }
157
158 if (domClasses.size() > 0) {
159 final ClassConfiguration classConfiguration =
160 new ClassConfiguration(klass, domClasses.toArray(new Class<?>[0]), isJsObject,
161 className, extendedClassName);
162
163 process(classConfiguration, expectedBrowser);
164 return classConfiguration;
165 }
166 }
167
168 final JsxClass jsxClass = klass.getAnnotation(JsxClass.class);
169 if (jsxClass != null && isSupported(jsxClass.value(), expectedBrowser)) {
170
171 final Set<Class<?>> domClasses = new HashSet<>();
172 final Class<?> domClass = jsxClass.domClass();
173 if (domClass != null && domClass != Object.class) {
174 domClasses.add(domClass);
175 }
176
177 String className = jsxClass.className();
178 if (className.isEmpty()) {
179 className = null;
180 }
181
182 final String extendedClassName;
183 final Class<?> superClass = klass.getSuperclass();
184 if (superClass.getAnnotation(JsxClass.class) == null
185 && superClass.getAnnotation(JsxClasses.class) == null) {
186 extendedClassName = "";
187 }
188 else {
189 extendedClassName = superClass.getSimpleName();
190 }
191
192 final ClassConfiguration classConfiguration
193 = new ClassConfiguration(klass,
194 domClasses.toArray(new Class<?>[0]),
195 jsxClass.isJSObject(),
196 className,
197 extendedClassName);
198
199 process(classConfiguration, expectedBrowser);
200 return classConfiguration;
201 }
202 }
203 return null;
204 }
205
206 private static void process(final ClassConfiguration classConfiguration, final SupportedBrowser expectedBrowser) {
207 final Map<String, Method> allGetters = new ConcurrentHashMap<>();
208 final Map<String, Method> allSetters = new ConcurrentHashMap<>();
209
210 try {
211
212 classConfiguration.addSymbolConstant(SymbolKey.TO_STRING_TAG, classConfiguration.getHostClassSimpleName());
213
214 for (final Method method : classConfiguration.getHostClass().getDeclaredMethods()) {
215 for (final Annotation annotation : method.getAnnotations()) {
216 if (annotation instanceof JsxGetter jsxGetter) {
217 if (isSupported(jsxGetter.value(), expectedBrowser)) {
218 String property;
219 if (jsxGetter.propertyName().isEmpty()) {
220 final int prefix = method.getName().startsWith("is") ? 2 : 3;
221 property = method.getName().substring(prefix);
222 property = Character.toLowerCase(property.charAt(0)) + property.substring(1);
223 }
224 else {
225 property = jsxGetter.propertyName();
226 }
227 allGetters.put(property, method);
228 }
229 }
230 else if (annotation instanceof JsxSetter jsxSetter) {
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 jsxSymbol) {
244 if (isSupported(jsxSymbol.value(), expectedBrowser)) {
245 final String symbolKeyName;
246 if (jsxSymbol.symbolName().isEmpty()) {
247 symbolKeyName = method.getName();
248 }
249 else {
250 symbolKeyName = jsxSymbol.symbolName();
251 }
252
253 final SymbolKey symbolKey;
254 if ("iterator".equalsIgnoreCase(symbolKeyName)) {
255 symbolKey = SymbolKey.ITERATOR;
256 }
257 else {
258 throw new RuntimeException("Invalid JsxSymbol annotation; unsupported '"
259 + symbolKeyName + "' symbol name.");
260 }
261 classConfiguration.addSymbol(symbolKey, method);
262 }
263 }
264 else if (annotation instanceof JsxFunction jsxFunction) {
265 if (isSupported(jsxFunction.value(), expectedBrowser)) {
266 final String name;
267 if (jsxFunction.functionName().isEmpty()) {
268 name = method.getName();
269 }
270 else {
271 name = jsxFunction.functionName();
272 }
273 classConfiguration.addFunction(name, method);
274 }
275 }
276 else if (annotation instanceof JsxStaticGetter jsxStaticGetter) {
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 jsxStaticFunction) {
285 if (isSupported(jsxStaticFunction.value(), expectedBrowser)) {
286 final String name;
287 if (jsxStaticFunction.functionName().isEmpty()) {
288 name = method.getName();
289 }
290 else {
291 name = jsxStaticFunction.functionName();
292 }
293 classConfiguration.addStaticFunction(name, method);
294 }
295 }
296 else if (annotation instanceof JsxConstructor jsxConstructor) {
297 if (isSupported(jsxConstructor.value(), expectedBrowser)) {
298 final String name;
299 if (jsxConstructor.functionName().isEmpty()) {
300 name = classConfiguration.getClassName();
301 }
302 else {
303 name = jsxConstructor.functionName();
304 }
305 classConfiguration.setJSConstructor(name, method);
306 }
307 }
308 else if (annotation instanceof JsxConstructorAlias jsxConstructorAlias) {
309 if (isSupported(jsxConstructorAlias.value(), expectedBrowser)) {
310 classConfiguration.setJSConstructorAlias(jsxConstructorAlias.alias());
311 }
312 }
313 }
314 }
315
316 for (final Entry<String, Method> getterEntry : allGetters.entrySet()) {
317 final String property = getterEntry.getKey();
318 classConfiguration.addProperty(property, getterEntry.getValue(), allSetters.get(property));
319 }
320
321
322 for (final Field field : classConfiguration.getHostClass().getDeclaredFields()) {
323 for (final Annotation annotation : field.getAnnotations()) {
324 if (annotation instanceof JsxConstant jsxConstant) {
325 if (isSupported(jsxConstant.value(), expectedBrowser)) {
326 try {
327 classConfiguration.addConstant(field.getName(), field.get(null));
328 }
329 catch (final IllegalAccessException e) {
330 throw JavaScriptEngine.reportRuntimeError(
331 "Cannot get field '" + field.getName()
332 + "' for type: " + classConfiguration.getHostClass().getName()
333 + "reason: " + e.getMessage());
334 }
335 }
336 }
337 if (annotation instanceof JsxSymbolConstant jsxSymbolConstant) {
338 if (isSupported(jsxSymbolConstant.value(), expectedBrowser)) {
339 final SymbolKey symbolKey;
340 if (StringUtils.startsWithIgnoreCase(field.getName(), "TO_STRING_TAG")) {
341 symbolKey = SymbolKey.TO_STRING_TAG;
342 }
343 else {
344 throw new RuntimeException("Invalid JsxSymbol annotation; unsupported '"
345 + field.getName() + "' symbol name.");
346 }
347 classConfiguration.addSymbolConstant(symbolKey, field.get(null).toString());
348 }
349 }
350 }
351 }
352 }
353 catch (final Throwable e) {
354 throw new RuntimeException(
355 "Processing classConfiguration for class "
356 + classConfiguration.getHostClassSimpleName() + "failed."
357 + " Reason: " + e, e);
358 }
359 }
360
361 private static boolean isSupported(final SupportedBrowser[] browsers, final SupportedBrowser expectedBrowser) {
362 for (final SupportedBrowser browser : browsers) {
363 if (browser == expectedBrowser) {
364 return true;
365 }
366 }
367 return false;
368 }
369
370
371
372
373
374
375
376
377 public Class<? extends HtmlUnitScriptable> getDomJavaScriptMappingFor(final Class<?> clazz) {
378 if (domJavaScriptMap_ == null) {
379 final Map<Class<?>, Class<? extends HtmlUnitScriptable>> map =
380 new ConcurrentHashMap<>(configuration_.size());
381
382 final boolean debug = LOG.isDebugEnabled();
383 for (final ClassConfiguration classConfig : configuration_) {
384 for (final Class<?> domClass : classConfig.getDomClasses()) {
385
386 if (debug) {
387 LOG.debug("Mapping " + domClass.getName() + " to " + classConfig.getClassName());
388 }
389 map.put(domClass, classConfig.getHostClass());
390 }
391 }
392
393 domJavaScriptMap_ = map;
394 }
395
396 return domJavaScriptMap_.get(clazz);
397 }
398
399 protected ClassConfiguration getGlobalThisConfiguration() {
400 return globalThisConfiguration_;
401 }
402 }