1 /*
   2  * Copyright (c) 2011, 2017, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.javafx.webkit.drt;
  27 
  28 import com.sun.javafx.application.PlatformImpl;
  29 import com.sun.javafx.logging.PlatformLogger;
  30 import com.sun.javafx.logging.PlatformLogger.Level;
  31 import com.sun.webkit.*;
  32 import com.sun.webkit.graphics.*;
  33 
  34 import static com.sun.webkit.network.URLs.newURL;
  35 import java.io.BufferedReader;
  36 import java.io.BufferedWriter;
  37 import java.io.File;
  38 import java.io.InputStreamReader;
  39 import java.io.OutputStreamWriter;
  40 import java.io.PrintWriter;
  41 import java.io.UnsupportedEncodingException;
  42 import java.net.MalformedURLException;
  43 import java.net.URL;
  44 import java.nio.ByteBuffer;
  45 import java.util.Date;
  46 import java.util.List;
  47 import java.util.concurrent.CountDownLatch;
  48 import javafx.scene.web.WebEngine;
  49 
  50 public final class DumpRenderTree {
  51     private final static PlatformLogger log = PlatformLogger.getLogger("DumpRenderTree");
  52     private final static long PID = (new Date()).getTime() & 0xFFFF;
  53     private final static String fileSep = System.getProperty("file.separator");
  54     private static boolean forceDumpAsText = false;
  55 
  56     final static PrintWriter out;
  57     static {
  58         try {
  59             out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(
  60                     System.out, "UTF-8")), true);
  61         } catch (UnsupportedEncodingException ex) {
  62             throw new RuntimeException(ex);
  63         }
  64     }
  65     static volatile DumpRenderTree drt;
  66 
  67     private final WebPage webPage;
  68     private final UIClientImpl uiClient;
  69     private final EventSender eventSender;
  70 
  71     private CountDownLatch latch;
  72     private String testPath;
  73     private boolean loaded;
  74     private boolean waiting;
  75     private boolean complete;
  76 
  77     static class ThemeClientImplStub extends ThemeClient {
  78         @Override
  79         protected RenderTheme createRenderTheme() {
  80             return new RenderThemeStub();
  81         }
  82 
  83         @Override
  84         protected ScrollBarTheme createScrollBarTheme() {
  85             return new ScrollBarThemeStub();
  86         }
  87     };
  88 
  89     static class RenderThemeStub extends RenderTheme {
  90         @Override
  91         protected Ref createWidget(long id, int widgetIndex, int state, int w, int h, int bgColor, ByteBuffer extParams) {
  92             return null;
  93         }
  94 
  95         @Override
  96         public void drawWidget(WCGraphicsContext g, Ref widget, int x, int y) {
  97         }
  98 
  99         @Override
 100         protected int getRadioButtonSize() {
 101             return 0;
 102         }
 103 
 104         @Override
 105         protected int getSelectionColor(int index) {
 106             return 0;
 107         }
 108 
 109         @Override
 110         public WCSize getWidgetSize(Ref widget) {
 111             return new WCSize(0, 0);
 112         }
 113     }
 114 
 115     static class ScrollBarThemeStub extends ScrollBarTheme {
 116         @Override
 117         protected Ref createWidget(long id, int w, int h, int orientation, int value, int visibleSize, int totalSize) {
 118             return null;
 119         }
 120 
 121         @Override
 122         protected void getScrollBarPartRect(long id, int part, int rect[]) {}
 123 
 124         @Override
 125         public void paint(WCGraphicsContext g, Ref sbRef, int x, int y, int pressedPart, int hoveredPart) {
 126         }
 127 
 128         @Override
 129         public WCSize getWidgetSize(Ref widget) {
 130             return new WCSize(0, 0);
 131         }
 132     }
 133 
 134     // called on FX thread
 135     private DumpRenderTree() {
 136         uiClient = new UIClientImpl();
 137         webPage = new WebPage(new WebPageClientImpl(), uiClient, null, null,
 138                               new ThemeClientImplStub(), false);
 139         uiClient.setWebPage(webPage);
 140         eventSender = new EventSender(webPage);
 141 
 142         webPage.setBounds(0, 0, 800, 600);
 143         webPage.setDeveloperExtrasEnabled(true);
 144         webPage.addLoadListenerClient(new DRTLoadListener());
 145 
 146     }
 147 
 148     private String getTestPath(String testString) {
 149         int t = testString.indexOf("'");
 150         String pixelsHash = "";
 151         if ((t > 0) && (t < testString.length() - 1)) {
 152             pixelsHash = testString.substring(t + 1);
 153             testString = testString.substring(0, t);
 154         }
 155         this.testPath = testString;
 156         init(testString, pixelsHash);
 157         return testString;
 158     }
 159 
 160 /*
 161     private static boolean isDebug()
 162     {
 163         return log.isLoggable(Level.FINE);
 164     }
 165 */
 166 
 167     private static void mlog(String msg) {
 168         if (log.isLoggable(Level.FINE)) {
 169             log.fine("PID:" + Long.toHexString(PID)
 170                     + " TID:" + Thread.currentThread().getId()
 171                         + "(" + Thread.currentThread().getName() + ") "
 172                     + msg);
 173         }
 174     }
 175 
 176     private static void initPlatform() throws Exception {
 177         // initialize default toolkit
 178         final CountDownLatch latch = new CountDownLatch(1);
 179         PlatformImpl.startup(() -> {
 180             new WebEngine();    // initialize Webkit classes
 181             System.loadLibrary("DumpRenderTreeJava");
 182             drt = new DumpRenderTree();
 183             PageCache.setCapacity(1);
 184             latch.countDown();
 185         });
 186         // wait for libraries to load
 187         latch.await();
 188     }
 189 
 190     boolean complete() { return this.complete; }
 191 
 192     private void reset() {
 193         mlog("reset");
 194         // Reset native objects associated with WebPage
 195         webPage.resetToConsistentStateBeforeTesting();
 196         // Clear frame name
 197         webPage.reset(webPage.getMainFrame());
 198         // Reset zoom factors
 199         webPage.setZoomFactor(1.0f, true);
 200         webPage.setZoomFactor(1.0f, false);
 201         // Reset DRT internal states
 202         complete = false;
 203         loaded = false;
 204         waiting = false;
 205     }
 206 
 207     // called on FX thread
 208     private void run(final String testString, final CountDownLatch latch) {
 209         this.latch = latch;
 210         String file = getTestPath(testString);
 211         mlog("{runTest: " + file);
 212         long mainFrame = webPage.getMainFrame();
 213         try {
 214             new URL(file);
 215         } catch (MalformedURLException ex) {
 216             file = "file:///" + file;
 217         }
 218         reset();
 219         webPage.open(mainFrame, file);
 220         mlog("}runTest");
 221     }
 222 
 223     private void runTest(final String testString) throws Exception {
 224         final CountDownLatch l = new CountDownLatch(1);
 225         Invoker.getInvoker().invokeOnEventThread(() -> {
 226             run(testString, l);
 227         });
 228         // wait until test is finished
 229         l.await();
 230         Invoker.getInvoker().invokeOnEventThread(() -> {
 231             mlog("dispose");
 232             webPage.stop();
 233             dispose();
 234         });
 235     }
 236 
 237     // called from native
 238     private static void waitUntilDone() {
 239         mlog("waitUntilDone");
 240         drt.setWaiting(true); // TODO: handle timeout
 241     }
 242 
 243     // called from native
 244     private static void notifyDone() {
 245         mlog("notifyDone");
 246         drt.setWaiting(false);
 247     }
 248 
 249     private static void overridePreference(String key, String value) {
 250         mlog("overridePreference");
 251         drt.webPage.overridePreference(key, value);
 252     }
 253 
 254     private synchronized void setLoaded(boolean loaded) {
 255         this.loaded = loaded;
 256         done();
 257     }
 258 
 259     private synchronized void setWaiting(boolean waiting) {
 260         this.waiting = waiting;
 261         done();
 262     }
 263 
 264     private synchronized void dump(long frame) {
 265         boolean dumpAsText = dumpAsText() || forceDumpAsText;
 266         mlog("dumpAsText = " + dumpAsText);
 267         if (dumpAsText) {
 268             String innerText = webPage.getInnerText(frame);
 269             if (frame == webPage.getMainFrame()) {
 270                 if (innerText != null) {
 271                     // don't use println() here as it varies from platform
 272                     // to platform, but DRT expects it always to be 0x0A
 273                     out.print(innerText + '\n');
 274                 }
 275             } else {
 276                 out.printf("\n--------\nFrame: '%s'\n--------\n%s\n",
 277                         webPage.getName(frame), innerText);
 278             }
 279             if (dumpChildFramesAsText()) {
 280                 List<Long> children = webPage.getChildFrames(frame);
 281                 if (children != null) {
 282                     for (long child : children) {
 283                         dump(child);
 284                     }
 285                 }
 286             }
 287             if (dumpBackForwardList() && frame == webPage.getMainFrame()) {
 288                 drt.dumpBfl();
 289             }
 290         } else {
 291             String renderTree = webPage.getRenderTree(frame);
 292             out.print(renderTree);
 293         }
 294     }
 295 
 296     private synchronized void done() {
 297         if (waiting || !loaded || complete) {
 298             return;
 299         }
 300         mlog("dump");
 301         dump(webPage.getMainFrame());
 302 
 303         mlog("done");
 304         out.print("#EOF" + '\n');
 305         // TODO: dump pixels here
 306         out.print("#EOF" + '\n');
 307         out.flush();
 308 
 309         System.err.print("#EOF" + '\n');
 310         System.err.flush();
 311 
 312         complete = true;
 313         // notify main thread that test is finished
 314         this.latch.countDown();
 315     }
 316 
 317     private static native void init(String testPath, String pixelsHash);
 318     private static native void didClearWindowObject(long pContext,
 319             long pWindowObject, EventSender eventSender);
 320     private static native void dispose();
 321 
 322     private static native boolean dumpAsText();
 323     private static native boolean dumpChildFramesAsText();
 324     private static native boolean dumpBackForwardList();
 325     protected static native boolean shouldStayOnPageAfterHandlingBeforeUnload();
 326 
 327     private final class DRTLoadListener implements LoadListenerClient {
 328         @Override
 329         public void dispatchLoadEvent(long frame, int state,
 330                                       String url, String contentType,
 331                                       double progress, int errorCode)
 332         {
 333             mlog("dispatchLoadEvent: ENTER");
 334             if (frame == webPage.getMainFrame()) {
 335                 mlog("dispatchLoadEvent: STATE = " + state);
 336                 switch (state) {
 337                     case PAGE_STARTED:
 338                         mlog("PAGE_STARTED");
 339                         setLoaded(false);
 340                         break;
 341                     case PAGE_FINISHED:
 342                         mlog("PAGE_FINISHED");
 343                         if (didFinishLoad()) {
 344                             setLoaded(true);
 345                         }
 346                         break;
 347                     case DOCUMENT_AVAILABLE:
 348                         dumpUnloadListeners(webPage, frame);
 349                         break;
 350                     case LOAD_FAILED:
 351                         mlog("LOAD_FAILED");
 352                         // safety net: if load fails, e.g. command line
 353                         // parameters were bad, let's not hang forever
 354                         setLoaded(true);
 355                         break;
 356                 }
 357             }
 358             mlog("dispatchLoadEvent: EXIT");
 359         }
 360         @Override
 361         public void dispatchResourceLoadEvent(long frame, int state,
 362                                               String url, String contentType,
 363                                               double progress, int errorCode)
 364         {
 365         }
 366     }
 367 
 368 
 369     public static void main(final String[] args) throws Exception {
 370 /*
 371         if ( isDebug() ) {
 372             // 'log' here is from java.util.logging
 373             log.setLevel(Level.FINEST);
 374             FileHandler handler = new FileHandler("drt.log", true);
 375             handler.setFormatter(new Formatter() {
 376                 @Override
 377                 public String format(LogRecord record) {
 378                     return formatMessage(record) + "\n";
 379                 }
 380             });
 381             log.addHandler(handler);
 382         }
 383 */
 384         mlog("{main");
 385         initPlatform();
 386         assert drt != null;
 387         for (String arg: args) {
 388             if ("--dump-as-text".equals(arg)) {
 389                 forceDumpAsText = true;
 390             } else if ("-".equals(arg)) {
 391                 // read from stdin
 392                 BufferedReader in = new BufferedReader(
 393                         new InputStreamReader(System.in));
 394                 String testPath;
 395                 while ((testPath = in.readLine()) != null) {
 396                     drt.runTest(testPath);
 397                 }
 398                 in.close();
 399             } else {
 400                 drt.runTest(arg);
 401             }
 402         }
 403         PlatformImpl.exit();
 404         mlog("}main");
 405         System.exit(0); // workaround to kill media threads
 406     }
 407 
 408     // called from native
 409     private static int getWorkerThreadCount() {
 410         return WebPage.getWorkerThreadCount();
 411     }
 412 
 413     // called from native
 414     private static String resolveURL(String relativeURL) {
 415         String testDir = new File(drt.testPath).getParentFile().getPath();
 416         File f = new File(testDir, relativeURL);
 417         String url = "file:///" + f.toString().replace(fileSep, "/");
 418         mlog("resolveURL: " + url);
 419         return url;
 420     }
 421 
 422     // called from native
 423     private static void loadURL(String url) {
 424         drt.webPage.open(drt.webPage.getMainFrame(), url);
 425     }
 426 
 427     // called from native
 428     private static void goBackForward(int dist) {
 429         // TODO: honor the dist
 430         if (dist > 0) {
 431             drt.webPage.goForward();
 432         } else {
 433             drt.webPage.goBack();
 434         }
 435     }
 436 
 437     // called from native
 438     private static int getBackForwardItemCount() {
 439         return drt.getBackForwardList().size();
 440     }
 441 
 442     // called from native
 443     private static void clearBackForwardList() {
 444         drt.getBackForwardList().clearBackForwardListForDRT();
 445     }
 446 
 447     private static final String TEST_DIR_NAME = "LayoutTests";
 448     private static final int TEST_DIR_LEN = TEST_DIR_NAME.length();
 449     private static final String CUR_ITEM_STR = "curr->";
 450     private static final int CUR_ITEM_STR_LEN = CUR_ITEM_STR.length();
 451     private static final String INDENT = "    ";
 452 
 453     private BackForwardList bfl;
 454     private BackForwardList getBackForwardList() {
 455         if (bfl == null) {
 456             bfl = webPage.createBackForwardList();
 457         }
 458         return bfl;
 459     }
 460 
 461     private void dumpBfl() {
 462         out.print("\n============== Back Forward List ==============\n");
 463         getBackForwardList();
 464         BackForwardList.Entry curItem = bfl.getCurrentEntry();
 465         for (BackForwardList.Entry e: bfl.toArray()) {
 466             dumpBflItem(e, 2, e == curItem);
 467         }
 468         out.print("===============================================\n");
 469     }
 470 
 471     private void dumpBflItem(BackForwardList.Entry item, int indent, boolean isCurrent) {
 472         StringBuilder str = new StringBuilder();
 473         for (int i = indent; i > 0; i--) str.append(INDENT);
 474 
 475         if (isCurrent) str.replace(0, CUR_ITEM_STR_LEN, CUR_ITEM_STR);
 476 
 477         String url = item.getURL().toString();
 478         if (url.contains("file:/")) {
 479             String subUrl = url.substring(url.indexOf(TEST_DIR_NAME) + TEST_DIR_LEN + 1);
 480             str.append("(file test):" + subUrl);
 481         } else {
 482             str.append(url);
 483         }
 484         if (item.getTarget() != null) {
 485             str.append(" (in frame \"" + item.getTarget() + "\")");
 486         }
 487         if (item.isTargetItem()) {
 488             str.append("  **nav target**\n");
 489         } else {
 490             str.append("\n");
 491         }
 492         out.print(str);
 493         if (item.getChildren() != null)
 494             for (BackForwardList.Entry child: item.getChildren())
 495                 dumpBflItem(child, indent + 1, false);
 496     }
 497 
 498     void dumpUnloadListeners(WebPage page, long frame) {
 499         if (waiting == true && dumpAsText()) {
 500             String dump = getUnloadListenersDescription(page, frame);
 501             if (dump != null) {
 502                 out.print(dump + '\n');
 503             }
 504         }
 505     }
 506 
 507     private static String getUnloadListenersDescription(WebPage page, long frame) {
 508         int count = page.getUnloadEventListenersCount(frame);
 509         if (count > 0) {
 510             return getFrameDescription(page, frame) +
 511                    " - has " + count + " onunload handler(s)";
 512         }
 513         return null;
 514     }
 515 
 516     private static String getFrameDescription(WebPage page, long frame) {
 517         String name = page.getName(frame);
 518         if (frame == page.getMainFrame()) {
 519             return name == null ? "main frame" : "main frame " + name;
 520         }
 521         return name == null ? "frame (anonymous)" : "frame " + name;
 522     }
 523 
 524     private native static boolean didFinishLoad();
 525 
 526     private final class WebPageClientImpl implements WebPageClient<Void> {
 527 
 528         @Override
 529         public void setCursor(long cursorID) {
 530         }
 531 
 532         @Override
 533         public void setFocus(boolean focus) {
 534         }
 535 
 536         @Override
 537         public void transferFocus(boolean forward) {
 538         }
 539 
 540         @Override
 541         public void setTooltip(String tooltip) {
 542         }
 543 
 544         @Override
 545         public WCRectangle getScreenBounds(boolean available) {
 546             return null;
 547         }
 548 
 549         @Override
 550         public int getScreenDepth() {
 551             return 24;
 552         }
 553 
 554         @Override
 555         public Void getContainer() {
 556             return null;
 557         }
 558 
 559         @Override
 560         public WCPoint screenToWindow(WCPoint ptScreen) {
 561             return ptScreen;
 562         }
 563 
 564         @Override
 565         public WCPoint windowToScreen(WCPoint ptWindow) {
 566             return ptWindow;
 567         }
 568 
 569         @Override
 570         public WCPageBackBuffer createBackBuffer() {
 571             throw new UnsupportedOperationException();
 572         }
 573 
 574         @Override
 575         public boolean isBackBufferSupported() {
 576             return false;
 577         }
 578 
 579         @Override
 580         public void addMessageToConsole(String message, int lineNumber,
 581                                         String sourceId)
 582         {
 583             if (complete) {
 584                 return;
 585             }
 586             if (!message.isEmpty()) {
 587                 int pos = message.indexOf("file://");
 588                 if (pos != -1) {
 589                     String s1 = message.substring(0, pos);
 590                     String s2 = message.substring(pos);
 591                     try {
 592                         // Extract the last path component aka file name
 593                         s2 = new File(newURL(s2).getPath()).getName();
 594                     } catch (MalformedURLException ignore) {}
 595                     message = s1 + s2;
 596                 }
 597             }
 598             if (lineNumber == 0) {
 599                 out.printf("CONSOLE MESSAGE: %s\n", message);
 600             } else {
 601                 out.printf("CONSOLE MESSAGE: line %d: %s\n",
 602                            lineNumber, message);
 603             }
 604         }
 605 
 606         @Override
 607         public void didClearWindowObject(long context, long windowObject) {
 608             mlog("didClearWindowObject");
 609             DumpRenderTree.didClearWindowObject(context, windowObject,
 610                                                 eventSender);
 611         }
 612     }
 613 }