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.host;
16  
17  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_CONSOLE_HANDLE_WINDOW;
18  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.CHROME;
19  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.EDGE;
20  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.FF;
21  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.IE;
22  
23  import java.util.HashMap;
24  import java.util.Locale;
25  import java.util.Map;
26  import java.util.regex.Matcher;
27  import java.util.regex.Pattern;
28  
29  import com.gargoylesoftware.htmlunit.WebConsole;
30  import com.gargoylesoftware.htmlunit.WebConsole.Formatter;
31  import com.gargoylesoftware.htmlunit.WebWindow;
32  import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable;
33  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxClass;
34  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxConstructor;
35  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxFunction;
36  
37  import net.sourceforge.htmlunit.corejs.javascript.BaseFunction;
38  import net.sourceforge.htmlunit.corejs.javascript.Context;
39  import net.sourceforge.htmlunit.corejs.javascript.Delegator;
40  import net.sourceforge.htmlunit.corejs.javascript.Function;
41  import net.sourceforge.htmlunit.corejs.javascript.NativeArray;
42  import net.sourceforge.htmlunit.corejs.javascript.NativeFunction;
43  import net.sourceforge.htmlunit.corejs.javascript.NativeObject;
44  import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
45  import net.sourceforge.htmlunit.corejs.javascript.ScriptableObject;
46  
47  /**
48   * A JavaScript object for {@code Console}.
49   *
50   * @author Andrea Martino
51   */
52  @JsxClass(isJSObject = false, value = {FF, CHROME})
53  @JsxClass({IE, EDGE})
54  public class Console extends SimpleScriptable {
55  
56      private static final Map<String, Long> TIMERS = new HashMap<>();
57      private static Formatter FORMATTER_ = new ConsoleFormatter();
58  
59      private WebWindow webWindow_;
60  
61      /**
62       * Default constructor.
63       */
64      @JsxConstructor(EDGE)
65      public Console() {
66      }
67  
68      /**
69       * Sets the Window JavaScript object this console belongs to.
70       * @param webWindow the Window JavaScript object this console belongs to
71       */
72      public void setWebWindow(final WebWindow webWindow) {
73          webWindow_ = webWindow;
74      }
75  
76      /**
77       * This method performs logging to the console at {@code log} level.
78       * @param cx the JavaScript context
79       * @param thisObj the scriptable
80       * @param args the arguments passed into the method
81       * @param funObj the function
82       */
83      @JsxFunction
84      public static void log(final Context cx, final Scriptable thisObj,
85          final Object[] args, final Function funObj) {
86          final WebConsole webConsole = toWebConsole(thisObj);
87          final Formatter oldFormatter = webConsole.getFormatter();
88          webConsole.setFormatter(FORMATTER_);
89          webConsole.info(args);
90          webConsole.setFormatter(oldFormatter);
91      }
92  
93      private static WebConsole toWebConsole(Scriptable thisObj) {
94          if (thisObj instanceof Window
95                  && ((SimpleScriptable) thisObj).getDomNodeOrDie().hasFeature(JS_CONSOLE_HANDLE_WINDOW)) {
96              thisObj = ((Window) thisObj).getConsole();
97          }
98          if (thisObj instanceof Console) {
99              return ((Console) thisObj).getWebConsole();
100         }
101         throw Context.reportRuntimeError("TypeError: object does not implemennt interface Console");
102     }
103 
104     /**
105      * This method performs logging to the console at {@code info} level.
106      * @param cx the JavaScript context
107      * @param thisObj the scriptable
108      * @param args the arguments passed into the method
109      * @param funObj the function
110      */
111     @JsxFunction
112     public static void info(final Context cx, final Scriptable thisObj,
113         final Object[] args, final Function funObj) {
114         final WebConsole webConsole = toWebConsole(thisObj);
115         final Formatter oldFormatter = webConsole.getFormatter();
116         webConsole.setFormatter(FORMATTER_);
117         webConsole.info(args);
118         webConsole.setFormatter(oldFormatter);
119     }
120 
121     /**
122      * This method performs logging to the console at {@code warn} level.
123      * @param cx the JavaScript context
124      * @param thisObj the scriptable
125      * @param args the arguments passed into the method
126      * @param funObj the function
127      */
128     @JsxFunction
129     public static void warn(final Context cx, final Scriptable thisObj,
130         final Object[] args, final Function funObj) {
131         final WebConsole webConsole = toWebConsole(thisObj);
132         final Formatter oldFormatter = webConsole.getFormatter();
133         webConsole.setFormatter(FORMATTER_);
134         webConsole.warn(args);
135         webConsole.setFormatter(oldFormatter);
136     }
137 
138     /**
139      * This method performs logging to the console at {@code error} level.
140      * @param cx the JavaScript context
141      * @param thisObj the scriptable
142      * @param args the arguments passed into the method
143      * @param funObj the function
144      */
145     @JsxFunction
146     public static void error(final Context cx, final Scriptable thisObj,
147         final Object[] args, final Function funObj) {
148         final WebConsole webConsole = toWebConsole(thisObj);
149         final Formatter oldFormatter = webConsole.getFormatter();
150         webConsole.setFormatter(FORMATTER_);
151         webConsole.error(args);
152         webConsole.setFormatter(oldFormatter);
153     }
154 
155     /**
156      * This method performs logging to the console at {@code debug} level.
157      * @param cx the JavaScript context
158      * @param thisObj the scriptable
159      * @param args the arguments passed into the method
160      * @param funObj the function
161      */
162     @JsxFunction
163     public static void debug(final Context cx, final Scriptable thisObj,
164         final Object[] args, final Function funObj) {
165         final WebConsole webConsole = toWebConsole(thisObj);
166         final Formatter oldFormatter = webConsole.getFormatter();
167         webConsole.setFormatter(FORMATTER_);
168         webConsole.debug(args);
169         webConsole.setFormatter(oldFormatter);
170     }
171 
172     /**
173      * This method performs logging to the console at {@code trace} level.
174      * @param cx the JavaScript context
175      * @param thisObj the scriptable
176      * @param args the arguments passed into the method
177      * @param funObj the function
178      */
179     @JsxFunction
180     public static void trace(final Context cx, final Scriptable thisObj,
181         final Object[] args, final Function funObj) {
182         final WebConsole webConsole = toWebConsole(thisObj);
183         final Formatter oldFormatter = webConsole.getFormatter();
184         webConsole.setFormatter(FORMATTER_);
185         webConsole.trace(args);
186         webConsole.setFormatter(oldFormatter);
187     }
188 
189     private WebConsole getWebConsole() {
190         return webWindow_.getWebClient().getWebConsole();
191     }
192 
193     /**
194      * Implementation of console {@code dir} function. This method does not enter recursively
195      * in the passed object, nor prints the details of objects or functions.
196      * @param o the object to be printed
197      */
198     @JsxFunction
199     public void dir(final Object o) {
200         if (o instanceof ScriptableObject) {
201             final ScriptableObject obj = (ScriptableObject) o;
202             final Object[] ids = obj.getIds();
203             if (ids != null && ids.length > 0) {
204                 final StringBuilder sb = new StringBuilder();
205                 for (Object id : ids) {
206                     final Object value = obj.get(id);
207                     if (value instanceof Delegator) {
208                         sb.append(id + ": " + ((Delegator) value).getClassName() + "\n");
209                     }
210                     else if (value instanceof SimpleScriptable) {
211                         sb.append(id + ": " + ((SimpleScriptable) value).getClassName() + "\n");
212                     }
213                     else if (value instanceof BaseFunction) {
214                         sb.append(id + ": function " + ((BaseFunction) value).getFunctionName() + "()\n");
215                     }
216                     else {
217                         sb.append(id + ": " + value  + "\n");
218                     }
219                 }
220                 getWebConsole().info(sb.toString());
221             }
222         }
223     }
224 
225     /**
226      * Implementation of group. Currently missing.
227      */
228     @JsxFunction
229     public void group() {
230         // TODO not implemented
231     }
232 
233     /**
234      * Implementation of endGroup. Currently missing.
235      */
236     @JsxFunction
237     public void groupEnd() {
238         // TODO not implemented
239     }
240 
241     /**
242      * Implementation of groupCollapsed. Currently missing.
243      */
244     @JsxFunction
245     public void groupCollapsed() {
246          // TODO not implemented
247     }
248 
249     /**
250      * This method replicates Firefox's behavior: if the timer already exists,
251      * the start time is not overwritten. In both cases, the line is printed on the
252      * console.
253      * @param timerName the name of the timer
254      */
255     @JsxFunction
256     public void time(final String timerName) {
257         if (!TIMERS.containsKey(timerName)) {
258             TIMERS.put(timerName, Long.valueOf(System.currentTimeMillis()));
259         }
260         getWebConsole().info(timerName + ": timer started");
261     }
262 
263     /**
264      * This method replicates Firefox's behavior: if no timer is found, nothing is
265      * logged to the console.
266      * @param timerName the name of the timer
267      */
268     @JsxFunction
269     public void timeEnd(final String timerName) {
270         final Long startTime = TIMERS.remove(timerName);
271         if (startTime != null) {
272             getWebConsole().info(timerName + ": " + (System.currentTimeMillis() - startTime.longValue()) + "ms");
273         }
274     }
275 
276     /**
277      * Because there is no timeline in HtmlUnit this does nothing.
278      * @param label the label
279      */
280     @JsxFunction({CHROME, FF})
281     public void timeStamp(final String label) {
282     }
283 
284     /**
285      * This class is the default formatter used by Console.
286      */
287     private static class ConsoleFormatter implements Formatter {
288 
289         private static void appendNativeArray(final NativeArray a, final StringBuilder sb, final int level) {
290             sb.append("[");
291             if (level < 3) {
292                 for (int i = 0; i < a.size(); i++) {
293                     if (i > 0) {
294                         sb.append(", ");
295                     }
296                     final Object val = a.get(i);
297                     if (val != null) {
298                         appendValue(val, sb, level + 1);
299                     }
300                 }
301             }
302             sb.append("]");
303         }
304 
305         private static void appendNativeObject(final NativeObject obj, final StringBuilder sb, final int level) {
306             if (level == 0) {
307                 // For whatever reason, when a native object is printed at the
308                 // root level Firefox puts brackets outside it. This is not the
309                 // case when a native object is printed as part of an array or
310                 // inside another object.
311                 sb.append("(");
312             }
313             sb.append("{");
314             if (level < 3) {
315                 final Object[] ids = obj.getIds();
316                 if (ids != null && ids.length > 0) {
317                     boolean needsSeparator = false;
318                     for (Object key : ids) {
319                         if (needsSeparator) {
320                             sb.append(", ");
321                         }
322                         sb.append(key);
323                         sb.append(":");
324                         appendValue(obj.get(key), sb, level + 1);
325                         needsSeparator = true;
326                     }
327                 }
328             }
329             sb.append("}");
330             if (level == 0) {
331                 sb.append(")");
332             }
333         }
334 
335         /**
336          * This methods appends the val parameter to the passed StringBuffer.
337          * FireBug's console prints some object differently if printed at the
338          * root level or as part of an array or native object. This method tries
339          * to simulate Firebus's behavior.
340          *
341          * @param val
342          *            the object to be printed
343          * @param sb
344          *            the StringBuilder used as destination
345          * @param level
346          *            the recursion level. If zero, it mean the object is
347          *            printed at the console root level. Otherwise, the object
348          *            is printed as part of an array or a native object.
349          */
350         private static void appendValue(final Object val, final StringBuilder sb, final int level) {
351             if (val instanceof NativeFunction) {
352                 sb.append("(");
353                 // Remove unnecessary new lines and spaces from the function
354                 final Pattern p = Pattern.compile("[ \\t]*\\r?\\n[ \\t]*",
355                         Pattern.MULTILINE);
356                 final Matcher m = p.matcher(((NativeFunction) val).toString());
357                 sb.append(m.replaceAll(" ").trim());
358                 sb.append(")");
359             }
360             else if (val instanceof BaseFunction) {
361                 sb.append("function ");
362                 sb.append(((BaseFunction) val).getFunctionName());
363                 sb.append("() {[native code]}");
364             }
365             else if (val instanceof NativeObject) {
366                 appendNativeObject((NativeObject) val, sb, level);
367             }
368             else if (val instanceof NativeArray) {
369                 appendNativeArray((NativeArray) val, sb, level);
370             }
371             else if (val instanceof Delegator) {
372                 if (level == 0) {
373                     sb.append("[object ");
374                     sb.append(((Delegator) val).getDelegee().getClassName());
375                     sb.append("]");
376                 }
377                 else {
378                     sb.append("({})");
379                 }
380             }
381             else if (val instanceof SimpleScriptable) {
382                 if (level == 0) {
383                     sb.append("[object ");
384                     sb.append(((SimpleScriptable) val).getClassName());
385                     sb.append("]");
386                 }
387                 else {
388                     sb.append("({})");
389                 }
390             }
391             else if (val instanceof String) {
392                 if (level == 0) {
393                     sb.append((String) val);
394                 }
395                 else {
396                     // When printing a string as part of an array or native
397                     // object,
398                     // enclose it in double quotes and escape its content.
399                     sb.append(quote((String) val));
400                 }
401             }
402             else if (val instanceof Number) {
403                 sb.append(((Number) val).toString());
404             }
405             else {
406                 // ?!?
407                 sb.append(val);
408             }
409         }
410 
411         /**
412          * Even if similar, this is not JSON encoding. This replicates the way
413          * Firefox console prints strings when logging.
414          * @param s the string to be quoted
415          */
416         private static String quote(final CharSequence s) {
417             final StringBuilder sb = new StringBuilder();
418             sb.append("\"");
419             for (int i = 0; i < s.length(); i++) {
420                 final char ch = s.charAt(i);
421                 switch (ch) {
422                     case '\\':
423                         sb.append("\\\\");
424                         break;
425                     case '\"':
426                         sb.append("\\\"");
427                         break;
428                     case '\b':
429                         sb.append("\\b");
430                         break;
431                     case '\t':
432                         sb.append("\\t");
433                         break;
434                     case '\n':
435                         sb.append("\\n");
436                         break;
437                     case '\f':
438                         sb.append("\\f");
439                         break;
440                     case '\r':
441                         sb.append("\\r");
442                         break;
443                     default:
444                         if (ch < ' ' || ch > '~') {
445                             sb.append("\\u" + Integer.toHexString(ch).toUpperCase(Locale.ROOT));
446                         }
447                         else {
448                             sb.append(ch);
449                         }
450                 }
451             }
452             sb.append("\"");
453             return sb.toString();
454         }
455 
456         private static String formatToString(final Object o) {
457             if (o == null) {
458                 return "null";
459             }
460             else if (o instanceof NativeFunction) {
461                 return ((NativeFunction) o).toString();
462             }
463             else if (o instanceof BaseFunction) {
464                 return "function " + ((BaseFunction) o).getFunctionName()
465                         + "\n" + "    [native code]\n" + "}";
466             }
467             else if (o instanceof NativeArray) {
468                 // If an array is embedded inside another array, just return
469                 // "[object Object]"
470                 return "[object Object]";
471             }
472             else if (o instanceof Delegator) {
473                 return "[object " + ((Delegator) o).getDelegee().getClassName()
474                         + "]";
475             }
476             else if (o instanceof NativeObject) {
477                 return "[object " + ((NativeObject) o).getClassName() + "]";
478             }
479             else if (o instanceof SimpleScriptable) {
480                 return "[object " + ((SimpleScriptable) o).getClassName() + "]";
481             }
482             else {
483                 return o.toString();
484             }
485         }
486 
487         @Override
488         public String printObject(final Object o) {
489             final StringBuilder sb = new StringBuilder();
490             appendValue(o, sb, 0);
491             return sb.toString();
492         }
493 
494         @Override
495         public String parameterAsString(final Object o) {
496             if (o instanceof NativeArray) {
497                 final StringBuilder sb = new StringBuilder();
498                 for (int i = 0; i < ((NativeArray) o).size(); i++) {
499                     if (i > 0) {
500                         sb.append(",");
501                     }
502                     sb.append(formatToString(((NativeArray) o).get(i)));
503                 }
504                 return sb.toString();
505             }
506             return formatToString(o);
507         }
508 
509         @Override
510         public String parameterAsInteger(final Object o) {
511             if (o instanceof Number) {
512                 return Integer.toString(((Number) o).intValue());
513             }
514             else if (o instanceof String) {
515                 try {
516                     return Integer.toString(Integer.parseInt((String) o));
517                 }
518                 catch (final NumberFormatException e) {
519                     // Swallow the exception and return below
520                 }
521             }
522             return "NaN";
523         }
524 
525         @Override
526         public String parameterAsFloat(final Object o) {
527             if (o instanceof Number) {
528                 return Float.toString(((Number) o).floatValue());
529             }
530             else if (o instanceof String) {
531                 try {
532                     return Float.toString(Float.parseFloat((String) o));
533                 }
534                 catch (final NumberFormatException e) {
535                     // Swallow the exception and return below
536                 }
537             }
538             return "NaN";
539         }
540     }
541 }