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.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
40
41
42
43
44
45
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
57
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
72
73 protected abstract Class<? extends HtmlUnitScriptable>[] getClasses();
74
75
76
77
78
79 public Iterable<ClassConfiguration> getAll() {
80 return configuration_.values();
81 }
82
83
84
85
86
87
88
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;
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
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
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
369
370
371
372
373
374
375 @Deprecated
376 public static boolean isCompatible(final SupportedBrowser browser1, final SupportedBrowser browser2) {
377 return browser1 == browser2;
378 }
379
380
381
382
383
384
385 public ClassConfiguration getClassConfiguration(final String hostClassName) {
386 return configuration_.get(hostClassName);
387 }
388
389
390
391
392
393
394
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
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 }