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.equals(BrowserVersion.CHROME)) {
100                 expectedBrowser = SupportedBrowser.CHROME;
101             }
102             else if (browser.equals(BrowserVersion.INTERNET_EXPLORER)) {
103                 expectedBrowser = SupportedBrowser.IE;
104             }
105             else if (browser.equals(BrowserVersion.FIREFOX_45)) {
106                 expectedBrowser = SupportedBrowser.FF45;
107             }
108             else if (browser.equals(BrowserVersion.FIREFOX_52)) {
109                 expectedBrowser = SupportedBrowser.FF52;
110             }
111             else {
112                 expectedBrowser = SupportedBrowser.EDGE;
113             }
114 
115             final String hostClassName = klass.getName();
116             final JsxClasses jsxClasses = klass.getAnnotation(JsxClasses.class);
117             if (jsxClasses != null) {
118                 if (klass.getAnnotation(JsxClass.class) != null) {
119                     throw new RuntimeException("Invalid JsxClasses/JsxClass annotation; class '"
120                         + hostClassName + "' has both.");
121                 }
122                 final JsxClass[] jsxClassValues = jsxClasses.value();
123                 if (jsxClassValues.length == 1) {
124                     throw new RuntimeException("No need to specify JsxClasses with a single JsxClass for "
125                             + hostClassName);
126                 }
127                 final Set<Class<?>> domClasses = new HashSet<>();
128 
129                 boolean isJsObject = false;
130                 String className = null;
131                 String extendedClassName = "";
132 
133                 final Class<?> superClass = klass.getSuperclass();
134                 if (superClass == SimpleScriptable.class) {
135                     extendedClassName = "";
136                 }
137                 else {
138                     extendedClassName = superClass.getSimpleName();
139                 }
140 
141                 for (int i = 0; i < jsxClassValues.length; i++) {
142                     final JsxClass jsxClass = jsxClassValues[i];
143 
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                         if (jsxClass.extendedClass() != Object.class) {
153                             if (jsxClass.extendedClass() == SimpleScriptable.class) {
154                                 extendedClassName = "";
155                             }
156                             else {
157                                 extendedClassName = jsxClass.extendedClass().getSimpleName();
158                             }
159                         }
160                     }
161                 }
162 
163                 final ClassConfiguration classConfiguration =
164                         new ClassConfiguration(klass, domClasses.toArray(new Class<?>[0]), isJsObject,
165                                 className, extendedClassName);
166 
167                 process(classConfiguration, hostClassName, expectedBrowser);
168                 return classConfiguration;
169             }
170 
171             final JsxClass jsxClass = klass.getAnnotation(JsxClass.class);
172             if (jsxClass != null && isSupported(jsxClass.value(), expectedBrowser)) {
173 
174                 final Set<Class<?>> domClasses = new HashSet<>();
175                 final Class<?> domClass = jsxClass.domClass();
176                 if (domClass != null && domClass != Object.class) {
177                     domClasses.add(domClass);
178                 }
179 
180                 String className = jsxClass.className();
181                 if (className.isEmpty()) {
182                     className = null;
183                 }
184                 String extendedClassName = "";
185 
186                 final Class<?> superClass = klass.getSuperclass();
187                 if (superClass != SimpleScriptable.class) {
188                     extendedClassName = superClass.getSimpleName();
189                 }
190                 else {
191                     extendedClassName = "";
192                 }
193                 if (jsxClass.extendedClass() != Object.class) {
194                     extendedClassName = jsxClass.extendedClass().getSimpleName();
195                 }
196 
197                 final ClassConfiguration classConfiguration
198                     = new ClassConfiguration(klass, domClasses.toArray(new Class<?>[0]), jsxClass.isJSObject(),
199                             className, extendedClassName);
200 
201                 process(classConfiguration, hostClassName, expectedBrowser);
202                 return classConfiguration;
203             }
204         }
205         return null;
206     }
207 
208     private static void process(final ClassConfiguration classConfiguration,
209             final String hostClassName, final SupportedBrowser expectedBrowser) {
210         final String simpleClassName = hostClassName.substring(hostClassName.lastIndexOf('.') + 1);
211 
212         CLASS_NAME_MAP_.put(hostClassName, simpleClassName);
213         final Map<String, Method> allGetters = new HashMap<>();
214         final Map<String, Method> allSetters = new HashMap<>();
215         for (final Constructor<?> constructor : classConfiguration.getHostClass().getDeclaredConstructors()) {
216             for (final Annotation annotation : constructor.getAnnotations()) {
217                 if (annotation instanceof JsxConstructor && isSupported(((JsxConstructor) annotation).value(),
218                         expectedBrowser)) {
219                     classConfiguration.setJSConstructor(constructor);
220                 }
221             }
222         }
223         for (final Method method : classConfiguration.getHostClass().getDeclaredMethods()) {
224             for (final Annotation annotation : method.getAnnotations()) {
225                 if (annotation instanceof JsxGetter) {
226                     final JsxGetter jsxGetter = (JsxGetter) annotation;
227                     if (isSupported(jsxGetter.value(), expectedBrowser)) {
228                         String property;
229                         if (jsxGetter.propertyName().isEmpty()) {
230                             final int prefix = method.getName().startsWith("is") ? 2 : 3;
231                             property = method.getName().substring(prefix);
232                             property = Character.toLowerCase(property.charAt(0)) + property.substring(1);
233                         }
234                         else {
235                             property = jsxGetter.propertyName();
236                         }
237                         allGetters.put(property, method);
238                     }
239                 }
240                 else if (annotation instanceof JsxSetter) {
241                     final JsxSetter jsxSetter = (JsxSetter) annotation;
242                     if (isSupported(jsxSetter.value(), expectedBrowser)) {
243                         String property;
244                         if (jsxSetter.propertyName().isEmpty()) {
245                             property = method.getName().substring(3);
246                             property = Character.toLowerCase(property.charAt(0)) + property.substring(1);
247                         }
248                         else {
249                             property = jsxSetter.propertyName();
250                         }
251                         allSetters.put(property, 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 && isSupported(((JsxConstructor) annotation).value(),
290                         expectedBrowser)) {
291                     classConfiguration.setJSConstructor(method);
292                 }
293             }
294         }
295         for (final Field field : classConfiguration.getHostClass().getDeclaredFields()) {
296             final JsxConstant jsxConstant = field.getAnnotation(JsxConstant.class);
297             if (jsxConstant != null && isSupported(jsxConstant.value(), expectedBrowser)) {
298                 classConfiguration.addConstant(field.getName());
299             }
300         }
301         for (final Entry<String, Method> getterEntry : allGetters.entrySet()) {
302             final String property = getterEntry.getKey();
303             classConfiguration.addProperty(property, getterEntry.getValue(), allSetters.get(property));
304         }
305     }
306 
307     private static boolean isSupported(final SupportedBrowser[] browsers, final SupportedBrowser expectedBrowser) {
308         for (final SupportedBrowser browser : browsers) {
309             if (isCompatible(browser, expectedBrowser)) {
310                 return true;
311             }
312         }
313         return false;
314     }
315 
316     /**
317      * Returns whether the two {@link SupportedBrowser} are compatible or not.
318      * @param browser1 the first {@link SupportedBrowser}
319      * @param browser2 the second {@link SupportedBrowser}
320      * @return whether the two {@link SupportedBrowser} are compatible or not
321      */
322     public static boolean isCompatible(final SupportedBrowser browser1, final SupportedBrowser browser2) {
323         return (browser1 == browser2)
324                 || (browser1 == FF && (browser2 == FF45 || browser2 == FF52))
325                 || (browser2 == FF && (browser1 == FF45 || browser1 == FF52));
326     }
327 
328     /**
329      * Gets the class configuration for the supplied JavaScript class name.
330      * @param hostClassName the JavaScript class name
331      * @return the class configuration for the supplied JavaScript class name
332      */
333     public ClassConfiguration getClassConfiguration(final String hostClassName) {
334         return configuration_.get(hostClassName);
335     }
336 
337     /**
338      * Returns an immutable map containing the DOM to JavaScript mappings. Keys are
339      * java classes for the various DOM classes (e.g. HtmlInput.class) and the values
340      * are the JavaScript class names (e.g. "HTMLAnchorElement").
341      * @return the mappings
342      */
343     public Map<Class<?>, Class<? extends HtmlUnitScriptable>> getDomJavaScriptMapping() {
344         if (domJavaScriptMap_ != null) {
345             return domJavaScriptMap_;
346         }
347 
348         final Map<Class<?>, Class<? extends HtmlUnitScriptable>> map = new HashMap<>(configuration_.size());
349 
350         final boolean debug = LOG.isDebugEnabled();
351         for (final String hostClassName : configuration_.keySet()) {
352             final ClassConfiguration classConfig = getClassConfiguration(hostClassName);
353             for (final Class<?> domClass : classConfig.getDomClasses()) {
354                 // preload and validate that the class exists
355                 if (debug) {
356                     LOG.debug("Mapping " + domClass.getName() + " to " + hostClassName);
357                 }
358                 map.put(domClass, classConfig.getHostClass());
359             }
360         }
361 
362         domJavaScriptMap_ = Collections.unmodifiableMap(map);
363 
364         return domJavaScriptMap_;
365     }
366 }