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
39
40
41
42
43
44
45
46
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
59
60
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
78
79 protected abstract Class<? extends HtmlUnitScriptable>[] getClasses();
80
81
82
83
84
85 public Iterable<ClassConfiguration> getAll() {
86 return configuration_;
87 }
88
89
90
91
92
93
94
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;
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
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
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
377
378
379
380
381
382
383 @Deprecated
384 public static boolean isCompatible(final SupportedBrowser browser1, final SupportedBrowser browser2) {
385 return browser1 == browser2;
386 }
387
388
389
390
391
392
393
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
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 }