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