View Javadoc
1   /*
2    * Copyright (c) 2002-2017 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    * http://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 com.gargoylesoftware.htmlunit.javascript.configuration;
16  
17  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.*;
18  import java.lang.annotation.Annotation;
19  import java.lang.reflect.Constructor;
20  import java.lang.reflect.Field;
21  import java.lang.reflect.Method;
22  import java.util.Collections;
23  import java.util.HashMap;
24  import java.util.HashSet;
25  import java.util.Map;
26  import java.util.Map.Entry;
27  import java.util.Set;
28  
29  import org.apache.commons.logging.Log;
30  import org.apache.commons.logging.LogFactory;
31  
32  import com.gargoylesoftware.htmlunit.BrowserVersion;
33  import com.gargoylesoftware.htmlunit.javascript.HtmlUnitScriptable;
34  import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable;
35  
36  /**
37   * An abstract container for all the JavaScript configuration information.
38   *
39   * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
40   * @author Chris Erskine
41   * @author Ahmed Ashour
42   * @author Ronald Brill
43   * @author Frank Danek
44   */
45  public abstract class AbstractJavaScriptConfiguration {
46  
47      private static final Log LOG = LogFactory.getLog(AbstractJavaScriptConfiguration.class);
48  
49      private static final Map<String, String> CLASS_NAME_MAP_ = new HashMap<>();
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_ = buildUsageMap(browser);
61      }
62  
63      /**
64       * @return the classes configured by this configuration
65       */
66      protected abstract Class<? extends SimpleScriptable>[] getClasses();
67  
68      /**
69       * Gets all the configurations.
70       * @return the class configurations
71       */
72      public Iterable<ClassConfiguration> getAll() {
73          return configuration_.values();
74      }
75  
76      private Map<String, ClassConfiguration> buildUsageMap(final BrowserVersion browser) {
77          final Map<String, ClassConfiguration> classMap = new HashMap<>(getClasses().length);
78  
79          for (final Class<? extends SimpleScriptable> klass : getClasses()) {
80              final ClassConfiguration config = getClassConfiguration(klass, browser);
81              if (config != null) {
82                  classMap.put(config.getClassName(), config);
83              }
84          }
85          return Collections.unmodifiableMap(classMap);
86      }
87  
88      /**
89       * Returns the class configuration of the given {@code klass}.
90       *
91       * @param klass the class
92       * @param browser the browser version
93       * @return the class configuration
94       */
95      public static ClassConfiguration getClassConfiguration(final Class<? extends HtmlUnitScriptable> klass,
96          final BrowserVersion browser) {
97          if (browser != null) {
98              final SupportedBrowser expectedBrowser;
99              if (browser.isChrome()) {
100                 expectedBrowser = SupportedBrowser.CHROME;
101             }
102             else if (browser.isIE()) {
103                 expectedBrowser = SupportedBrowser.IE;
104             }
105             else if (browser.isEdge()) {
106                 expectedBrowser = SupportedBrowser.EDGE;
107             }
108             else if (browser.isFirefox52()) {
109                 expectedBrowser = SupportedBrowser.FF52;
110             }
111             else if (browser.isFirefox()) {
112                 expectedBrowser = SupportedBrowser.FF45;
113             }
114             else {
115                 expectedBrowser = SupportedBrowser.CHROME;  // our current fallback
116             }
117 
118             final String hostClassName = klass.getName();
119             final JsxClasses jsxClasses = klass.getAnnotation(JsxClasses.class);
120             if (jsxClasses != null) {
121                 if (klass.getAnnotation(JsxClass.class) != null) {
122                     throw new RuntimeException("Invalid JsxClasses/JsxClass annotation; class '"
123                         + hostClassName + "' has both.");
124                 }
125                 final JsxClass[] jsxClassValues = jsxClasses.value();
126                 if (jsxClassValues.length == 1) {
127                     throw new RuntimeException("No need to specify JsxClasses with a single JsxClass for "
128                             + hostClassName);
129                 }
130                 final Set<Class<?>> domClasses = new HashSet<>();
131 
132                 boolean isJsObject = false;
133                 String className = null;
134                 String extendedClassName = "";
135 
136                 final Class<?> superClass = klass.getSuperclass();
137                 if (superClass == SimpleScriptable.class) {
138                     extendedClassName = "";
139                 }
140                 else {
141                     extendedClassName = superClass.getSimpleName();
142                 }
143 
144                 for (int i = 0; i < jsxClassValues.length; i++) {
145                     final JsxClass jsxClass = jsxClassValues[i];
146 
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                         if (jsxClass.extendedClass() != Object.class) {
156                             if (jsxClass.extendedClass() == SimpleScriptable.class) {
157                                 extendedClassName = "";
158                             }
159                             else {
160                                 extendedClassName = jsxClass.extendedClass().getSimpleName();
161                             }
162                         }
163                     }
164                 }
165 
166                 final ClassConfiguration classConfiguration =
167                         new ClassConfiguration(klass, domClasses.toArray(new Class<?>[0]), isJsObject,
168                                 className, extendedClassName);
169 
170                 process(classConfiguration, hostClassName, expectedBrowser);
171                 return classConfiguration;
172             }
173 
174             final JsxClass jsxClass = klass.getAnnotation(JsxClass.class);
175             if (jsxClass != null && isSupported(jsxClass.value(), expectedBrowser)) {
176 
177                 final Set<Class<?>> domClasses = new HashSet<>();
178                 final Class<?> domClass = jsxClass.domClass();
179                 if (domClass != null && domClass != Object.class) {
180                     domClasses.add(domClass);
181                 }
182 
183                 String className = jsxClass.className();
184                 if (className.isEmpty()) {
185                     className = null;
186                 }
187                 String extendedClassName = "";
188 
189                 final Class<?> superClass = klass.getSuperclass();
190                 if (superClass != SimpleScriptable.class) {
191                     extendedClassName = superClass.getSimpleName();
192                 }
193                 else {
194                     extendedClassName = "";
195                 }
196                 if (jsxClass.extendedClass() != Object.class) {
197                     extendedClassName = jsxClass.extendedClass().getSimpleName();
198                 }
199 
200                 final ClassConfiguration classConfiguration
201                     = new ClassConfiguration(klass, domClasses.toArray(new Class<?>[0]), jsxClass.isJSObject(),
202                             className, extendedClassName);
203 
204                 process(classConfiguration, hostClassName, expectedBrowser);
205                 return classConfiguration;
206             }
207         }
208         return null;
209     }
210 
211     private static void process(final ClassConfiguration classConfiguration,
212             final String hostClassName, final SupportedBrowser expectedBrowser) {
213         final String simpleClassName = hostClassName.substring(hostClassName.lastIndexOf('.') + 1);
214 
215         CLASS_NAME_MAP_.put(hostClassName, simpleClassName);
216         final Map<String, Method> allGetters = new HashMap<>();
217         final Map<String, Method> allSetters = new HashMap<>();
218         for (final Constructor<?> constructor : classConfiguration.getHostClass().getDeclaredConstructors()) {
219             for (final Annotation annotation : constructor.getAnnotations()) {
220                 if (annotation instanceof JsxConstructor && isSupported(((JsxConstructor) annotation).value(),
221                         expectedBrowser)) {
222                     classConfiguration.setJSConstructor(constructor);
223                 }
224             }
225         }
226         for (final Method method : classConfiguration.getHostClass().getDeclaredMethods()) {
227             for (final Annotation annotation : method.getAnnotations()) {
228                 if (annotation instanceof JsxGetter) {
229                     final JsxGetter jsxGetter = (JsxGetter) annotation;
230                     if (isSupported(jsxGetter.value(), expectedBrowser)) {
231                         String property;
232                         if (jsxGetter.propertyName().isEmpty()) {
233                             final int prefix = method.getName().startsWith("is") ? 2 : 3;
234                             property = method.getName().substring(prefix);
235                             property = Character.toLowerCase(property.charAt(0)) + property.substring(1);
236                         }
237                         else {
238                             property = jsxGetter.propertyName();
239                         }
240                         allGetters.put(property, method);
241                     }
242                 }
243                 else if (annotation instanceof JsxSetter) {
244                     final JsxSetter jsxSetter = (JsxSetter) annotation;
245                     if (isSupported(jsxSetter.value(), expectedBrowser)) {
246                         String property;
247                         if (jsxSetter.propertyName().isEmpty()) {
248                             property = method.getName().substring(3);
249                             property = Character.toLowerCase(property.charAt(0)) + property.substring(1);
250                         }
251                         else {
252                             property = jsxSetter.propertyName();
253                         }
254                         allSetters.put(property, method);
255                     }
256                 }
257                 else if (annotation instanceof JsxFunction) {
258                     final JsxFunction jsxFunction = (JsxFunction) annotation;
259                     if (isSupported(jsxFunction.value(), expectedBrowser)) {
260                         final String name;
261                         if (jsxFunction.functionName().isEmpty()) {
262                             name = method.getName();
263                         }
264                         else {
265                             name = jsxFunction.functionName();
266                         }
267                         classConfiguration.addFunction(name, method);
268                     }
269                 }
270                 else if (annotation instanceof JsxStaticGetter) {
271                     final JsxStaticGetter jsxStaticGetter = (JsxStaticGetter) annotation;
272                     if (isSupported(jsxStaticGetter.value(), expectedBrowser)) {
273                         final int prefix = method.getName().startsWith("is") ? 2 : 3;
274                         String property = method.getName().substring(prefix);
275                         property = Character.toLowerCase(property.charAt(0)) + property.substring(1);
276                         classConfiguration.addStaticProperty(property, method, null);
277                     }
278                 }
279                 else if (annotation instanceof JsxStaticFunction) {
280                     final JsxStaticFunction jsxStaticFunction = (JsxStaticFunction) annotation;
281                     if (isSupported(jsxStaticFunction.value(), expectedBrowser)) {
282                         final String name;
283                         if (jsxStaticFunction.functionName().isEmpty()) {
284                             name = method.getName();
285                         }
286                         else {
287                             name = jsxStaticFunction.functionName();
288                         }
289                         classConfiguration.addStaticFunction(name, method);
290                     }
291                 }
292                 else if (annotation instanceof JsxConstructor && isSupported(((JsxConstructor) annotation).value(),
293                         expectedBrowser)) {
294                     classConfiguration.setJSConstructor(method);
295                 }
296             }
297         }
298         for (final Field field : classConfiguration.getHostClass().getDeclaredFields()) {
299             final JsxConstant jsxConstant = field.getAnnotation(JsxConstant.class);
300             if (jsxConstant != null && isSupported(jsxConstant.value(), expectedBrowser)) {
301                 classConfiguration.addConstant(field.getName());
302             }
303         }
304         for (final Entry<String, Method> getterEntry : allGetters.entrySet()) {
305             final String property = getterEntry.getKey();
306             classConfiguration.addProperty(property, getterEntry.getValue(), allSetters.get(property));
307         }
308     }
309 
310     private static boolean isSupported(final SupportedBrowser[] browsers, final SupportedBrowser expectedBrowser) {
311         for (final SupportedBrowser browser : browsers) {
312             if (isCompatible(browser, expectedBrowser)) {
313                 return true;
314             }
315         }
316         return false;
317     }
318 
319     /**
320      * Returns whether the two {@link SupportedBrowser} are compatible or not.
321      * @param browser1 the first {@link SupportedBrowser}
322      * @param browser2 the second {@link SupportedBrowser}
323      * @return whether the two {@link SupportedBrowser} are compatible or not
324      */
325     public static boolean isCompatible(final SupportedBrowser browser1, final SupportedBrowser browser2) {
326         return (browser1 == browser2)
327                 || (browser1 == FF && (browser2 == FF45 || browser2 == FF52))
328                 || (browser2 == FF && (browser1 == FF45 || browser1 == FF52));
329     }
330 
331     /**
332      * Gets the class configuration for the supplied JavaScript class name.
333      * @param hostClassName the JavaScript class name
334      * @return the class configuration for the supplied JavaScript class name
335      */
336     public ClassConfiguration getClassConfiguration(final String hostClassName) {
337         return configuration_.get(hostClassName);
338     }
339 
340     /**
341      * Returns an immutable map containing the DOM to JavaScript mappings. Keys are
342      * java classes for the various DOM classes (e.g. HtmlInput.class) and the values
343      * are the JavaScript class names (e.g. "HTMLAnchorElement").
344      * @return the mappings
345      */
346     public Map<Class<?>, Class<? extends HtmlUnitScriptable>> getDomJavaScriptMapping() {
347         if (domJavaScriptMap_ != null) {
348             return domJavaScriptMap_;
349         }
350 
351         final Map<Class<?>, Class<? extends HtmlUnitScriptable>> map = new HashMap<>(configuration_.size());
352 
353         final boolean debug = LOG.isDebugEnabled();
354         for (final String hostClassName : configuration_.keySet()) {
355             final ClassConfiguration classConfig = getClassConfiguration(hostClassName);
356             for (final Class<?> domClass : classConfig.getDomClasses()) {
357                 // preload and validate that the class exists
358                 if (debug) {
359                     LOG.debug("Mapping " + domClass.getName() + " to " + hostClassName);
360                 }
361                 map.put(domClass, classConfig.getHostClass());
362             }
363         }
364 
365         domJavaScriptMap_ = Collections.unmodifiableMap(map);
366 
367         return domJavaScriptMap_;
368     }
369 }