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.webkit.Accessor;
  30 import com.sun.javafx.webkit.ThemeClientImpl;
  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 java.util.logging.FileHandler;
  49 import java.util.logging.Formatter;
  50 import java.util.logging.Level;
  51 import java.util.logging.LogRecord;
  52 import java.util.logging.Logger;
  53 import javafx.scene.web.WebEngine;
  54 
  55 public final class DumpRenderTree {
  56     private final static Logger log = Logger.getLogger("DumpRenderTree");
  57     private final static long PID = (new Date()).getTime() & 0xFFFF;
  58     private final static String fileSep = System.getProperty("file.separator");
  59     private static boolean forceDumpAsText = false;
  60 
  61     final static PrintWriter out;
  62     static {
  63         try {
  64             out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(
  65                     System.out, "UTF-8")), true);
  66         } catch (UnsupportedEncodingException ex) {
  67             throw new RuntimeException(ex);
  68         }
  69     }
  70     static volatile DumpRenderTree drt;
  71 
  72     private final WebPage webPage;
  73     private final UIClientImpl uiClient;
  74     private final EventSender eventSender;
  75 
  76     private CountDownLatch latch;
  77     private String testPath;
  78     private boolean loaded;
  79     private boolean waiting;
  80     private boolean complete;
  81 
  82     class ThemeClientImplStub extends ThemeClient {
  83         @Override
  84         protected RenderTheme createRenderTheme() {
  85             return new RenderThemeStub();
  86         }
  87 
  88         @Override
  89         protected ScrollBarTheme createScrollBarTheme() {
  90             return new ScrollBarThemeStub();
  91         }
  92     };
  93 
  94     class RenderThemeStub extends RenderTheme {
  95         @Override
  96         protected Ref createWidget(long id, int widgetIndex, int state, int w, int h, int bgColor, ByteBuffer extParams) {
  97             return null;
  98         }
  99 
 100         @Override
 101         public void drawWidget(WCGraphicsContext g, Ref widget, int x, int y) {
 102         }
 103 
 104         @Override
 105         protected int getRadioButtonSize() {
 106             return 0;
 107         }
 108 
 109         @Override
 110         protected int getSelectionColor(int index) {
 111             return 0;
 112         }
 113 
 114         @Override
 115         public WCSize getWidgetSize(Ref widget) {
 116             return new WCSize(0, 0);
 117         }
 118     }
 119 
 120     class ScrollBarThemeStub extends ScrollBarTheme {
 121         @Override
 122         protected Ref createWidget(long id, int w, int h, int orientation, int value, int visibleSize, int totalSize) {
 123             return null;
 124         }
 125 
 126         @Override
 127         protected void getScrollBarPartRect(long id, int part, int rect[]) {}
 128 
 129         @Override
 130         public void paint(WCGraphicsContext g, Ref sbRef, int x, int y, int pressedPart, int hoveredPart) {
 131         }
 132 
 133         @Override
 134         public WCSize getWidgetSize(Ref widget) {
 135             return new WCSize(0, 0);
 136         }
 137     }
 138 
 139     // called on FX thread
 140     private DumpRenderTree() {
 141         uiClient = new UIClientImpl();
 142         webPage = new WebPage(new WebPageClientImpl(), uiClient, null, null,
 143                               new ThemeClientImplStub(), false);
 144         uiClient.setWebPage(webPage);
 145         eventSender = new EventSender(webPage);
 146 
 147         webPage.setBounds(0, 0, 800, 600);
 148         webPage.setUsePageCache(true);
 149         webPage.setDeveloperExtrasEnabled(true);
 150         webPage.addLoadListenerClient(new DRTLoadListener());
 151 
 152     }
 153 
 154     private String getTestPath(String testString) {
 155         int t = testString.indexOf("'");
 156         String pixelsHash = "";
 157         if ((t > 0) && (t < testString.length() - 1)) {
 158             pixelsHash = testString.substring(t + 1);
 159             testString = testString.substring(0, t);
 160         }
 161         this.testPath = testString;
 162         init(testString, pixelsHash);
 163         return testString;
 164     }
 165 
 166 
 167     private static boolean isDebug()
 168     {
 169         return log.isLoggable(Level.FINE);
 170     }
 171 
 172     private static void mlog(String msg)
 173     {
 174         if (isDebug()) {
 175             log.fine("PID:" + Long.toHexString(PID)
 176                     + " TID:" + Thread.currentThread().getId()
 177                         + "(" + Thread.currentThread().getName() + ") "
 178                     + msg);
 179         }
 180     }
 181 
 182     private static void initPlatform() throws Exception {
 183         // initialize default toolkit
 184         final CountDownLatch latch = new CountDownLatch(1);
 185         PlatformImpl.startup(() -> {
 186             new WebEngine();    // initialize Webkit classes
 187             System.loadLibrary("DumpRenderTreeJava");
 188             PageCache.setCapacity(1);
 189             drt = new DumpRenderTree();
 190             latch.countDown();
 191         });
 192         // wait for libraries to load
 193         latch.await();
 194     }
 195 
 196     private void reset() {
 197         mlog("reset");
 198         // Reset native objects associated with WebPage
 199         webPage.resetToConsistentStateBeforeTesting();
 200         // Reset zoom factors
 201         webPage.setZoomFactor(1.0f, true);
 202         webPage.setZoomFactor(1.0f, false);
 203         // Reset DRT internal states
 204         complete = false;
 205         loaded = false;
 206         waiting = false;
 207     }
 208 
 209     // called on FX thread
 210     private void run(final String testString, final CountDownLatch latch) {
 211         this.latch = latch;
 212         String file = getTestPath(testString);
 213         mlog("{runTest: " + file);
 214         long mainFrame = webPage.getMainFrame();
 215         try {
 216             new URL(file);
 217         } catch (MalformedURLException ex) {
 218             file = "file:///" + file;
 219         }
 220         reset();
 221         webPage.open(mainFrame, file);
 222         mlog("}runTest");
 223     }
 224 
 225     private void runTest(final String testString) throws Exception {
 226         final CountDownLatch l = new CountDownLatch(1);
 227         Invoker.getInvoker().invokeOnEventThread(() -> {
 228             run(testString, l);
 229         });
 230         // wait until test is finished
 231         l.await();
 232         Invoker.getInvoker().invokeOnEventThread(() -> {
 233             mlog("dispose");
 234             // drt.uiClient.closePage();
 235             dispose();
 236         });
 237     }
 238 
 239     // called from native
 240     private static void waitUntilDone() {
 241         mlog("waitUntilDone");
 242         drt.setWaiting(true); // TODO: handle timeout
 243     }
 244 
 245     // called from native
 246     private static void notifyDone() {
 247         mlog("notifyDone");
 248         drt.setWaiting(false);
 249     }
 250 
 251     private static void overridePreference(String key, String value) {
 252         mlog("overridePreference");
 253         drt.webPage.overridePreference(key, value);
 254     }
 255 
 256     private synchronized void setLoaded(boolean loaded) {
 257         this.loaded = loaded;
 258         done();
 259     }
 260 
 261     private synchronized void setWaiting(boolean waiting) {
 262         this.waiting = waiting;
 263         done();
 264     }
 265 
 266     private synchronized void dump(long frame) {
 267         boolean dumpAsText = dumpAsText() || forceDumpAsText;
 268         mlog("dumpAsText = " + dumpAsText);
 269         if (dumpAsText) {
 270             String innerText = webPage.getInnerText(frame);
 271             if (frame == webPage.getMainFrame()) {
 272                 if (innerText != null) {
 273                     // don't use println() here as it varies from platform
 274                     // to platform, but DRT expects it always to be 0x0A
 275                     out.print(innerText + '\n');
 276                 }
 277             } else {
 278                 out.printf("\n--------\nFrame: '%s'\n--------\n%s\n",
 279                         webPage.getName(frame), innerText);
 280             }
 281             if (dumpChildFramesAsText()) {
 282                 List<Long> children = webPage.getChildFrames(frame);
 283                 if (children != null) {
 284                     for (long child : children) {
 285                         dump(child);
 286                     }
 287                 }
 288             }
 289             if (dumpBackForwardList() && frame == webPage.getMainFrame()) {
 290                 drt.dumpBfl();
 291             }
 292         } else {
 293             String renderTree = webPage.getRenderTree(frame);
 294             out.print(renderTree);
 295         }
 296     }
 297 
 298     private synchronized void done() {
 299         if (waiting || !loaded || complete) {
 300             return;
 301         }
 302         mlog("dump");
 303         dump(webPage.getMainFrame());
 304 
 305         mlog("done");
 306         out.print("#EOF" + '\n');
 307         // TODO: dump pixels here
 308         out.print("#EOF" + '\n');
 309         out.flush();
 310 
 311         System.err.print("#EOF" + '\n');
 312         System.err.flush();
 313 
 314         complete = true;
 315         // notify main thread that test is finished
 316         this.latch.countDown();
 317     }
 318 
 319     private static native void init(String testPath, String pixelsHash);
 320     private static native void didClearWindowObject(long pContext,
 321             long pWindowObject, EventSender eventSender);
 322     private static native void dispose();
 323 
 324     private static native boolean dumpAsText();
 325     private static native boolean dumpChildFramesAsText();
 326     private static native boolean dumpBackForwardList();
 327     protected static native boolean shouldStayOnPageAfterHandlingBeforeUnload();
 328 
 329     private final class DRTLoadListener implements LoadListenerClient {
 330         @Override
 331         public void dispatchLoadEvent(long frame, int state,
 332                                       String url, String contentType,
 333                                       double progress, int errorCode)
 334         {
 335             mlog("dispatchLoadEvent: ENTER");
 336             if (frame == webPage.getMainFrame()) {
 337                 mlog("dispatchLoadEvent: STATE = " + state);
 338                 switch (state) {
 339                     case PAGE_STARTED:
 340                         mlog("PAGE_STARTED");
 341                         setLoaded(false);
 342                         break;
 343                     case PAGE_FINISHED:
 344                         mlog("PAGE_FINISHED");
 345                         if (didFinishLoad()) {
 346                             setLoaded(true);
 347                         }
 348                         break;
 349                     case DOCUMENT_AVAILABLE:
 350                         dumpUnloadListeners(webPage, frame);
 351                         break;
 352                     case LOAD_FAILED:
 353                         mlog("LOAD_FAILED");
 354                         // safety net: if load fails, e.g. command line
 355                         // parameters were bad, let's not hang forever
 356                         setLoaded(true);
 357                         break;
 358                 }
 359             }
 360             mlog("dispatchLoadEvent: EXIT");
 361         }
 362         @Override
 363         public void dispatchResourceLoadEvent(long frame, int state,
 364                                               String url, String contentType,
 365                                               double progress, int errorCode)
 366         {
 367         }
 368     }
 369 
 370 
 371     public static void main(final String[] args) throws Exception {
 372         if ( isDebug() ) {
 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 drt.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     private static final String TEST_DIR_NAME = "LayoutTests";
 443     private static final int TEST_DIR_LEN = TEST_DIR_NAME.length();
 444     private static final String CUR_ITEM_STR = "curr->";
 445     private static final int CUR_ITEM_STR_LEN = CUR_ITEM_STR.length();
 446     private static final String INDENT = "    ";
 447 
 448     private BackForwardList bfl;
 449     private BackForwardList getBackForwardList() {
 450         if (bfl == null) {
 451             bfl = webPage.createBackForwardList();
 452         }
 453         return bfl;
 454     }
 455 
 456     private void dumpBfl() {
 457         out.print("\n============== Back Forward List ==============\n");
 458         getBackForwardList();
 459         BackForwardList.Entry curItem = bfl.getCurrentEntry();
 460         for (BackForwardList.Entry e: bfl.toArray()) {
 461             dumpBflItem(e, 2, e == curItem);
 462         }
 463         out.print("===============================================\n");
 464     }
 465 
 466     private void dumpBflItem(BackForwardList.Entry item, int indent, boolean isCurrent) {
 467         StringBuilder str = new StringBuilder();
 468         for (int i = indent; i > 0; i--) str.append(INDENT);
 469 
 470         if (isCurrent) str.replace(0, CUR_ITEM_STR_LEN, CUR_ITEM_STR);
 471 
 472         String url = item.getURL().toString();
 473         if (url.contains("file:/")) {
 474             String subUrl = url.substring(url.indexOf(TEST_DIR_NAME) + TEST_DIR_LEN + 1);
 475             str.append("(file test):" + subUrl);
 476         } else {
 477             str.append(url);
 478         }
 479         if (item.getTarget() != null) {
 480             str.append(" (in frame \"" + item.getTarget() + "\")");
 481         }
 482         if (item.isTargetItem()) {
 483             str.append("  **nav target**\n");
 484         } else {
 485             str.append("\n");
 486         }
 487         out.print(str);
 488         if (item.getChildren() != null)
 489             for (BackForwardList.Entry child: item.getChildren())
 490                 dumpBflItem(child, indent + 1, false);
 491     }
 492 
 493     void dumpUnloadListeners(WebPage page, long frame) {
 494         if (waiting == true && dumpAsText()) {
 495             String dump = getUnloadListenersDescription(page, frame);
 496             if (dump != null) {
 497                 out.print(dump + '\n');
 498             }
 499         }
 500     }
 501 
 502     private static String getUnloadListenersDescription(WebPage page, long frame) {
 503         int count = page.getUnloadEventListenersCount(frame);
 504         if (count > 0) {
 505             return getFrameDescription(page, frame) +
 506                    " - has " + count + " onunload handler(s)";
 507         }
 508         return null;
 509     }
 510 
 511     private static String getFrameDescription(WebPage page, long frame) {
 512         String name = page.getName(frame);
 513         if (frame == page.getMainFrame()) {
 514             return name == null ? "main frame" : "main frame " + name;
 515         }
 516         return name == null ? "frame (anonymous)" : "frame " + name;
 517     }
 518 
 519     private native static boolean didFinishLoad();
 520 
 521     private final class WebPageClientImpl implements WebPageClient<Void> {
 522 
 523         @Override
 524         public void setCursor(long cursorID) {
 525         }
 526 
 527         @Override
 528         public void setFocus(boolean focus) {
 529         }
 530 
 531         @Override
 532         public void transferFocus(boolean forward) {
 533         }
 534 
 535         @Override
 536         public void setTooltip(String tooltip) {
 537         }
 538 
 539         @Override
 540         public WCRectangle getScreenBounds(boolean available) {
 541             return null;
 542         }
 543 
 544         @Override
 545         public int getScreenDepth() {
 546             return 24;
 547         }
 548 
 549         @Override
 550         public Void getContainer() {
 551             return null;
 552         }
 553 
 554         @Override
 555         public WCPoint screenToWindow(WCPoint ptScreen) {
 556             return ptScreen;
 557         }
 558 
 559         @Override
 560         public WCPoint windowToScreen(WCPoint ptWindow) {
 561             return ptWindow;
 562         }
 563 
 564         @Override
 565         public WCPageBackBuffer createBackBuffer() {
 566             throw new UnsupportedOperationException();
 567         }
 568 
 569         @Override
 570         public boolean isBackBufferSupported() {
 571             return false;
 572         }
 573 
 574         @Override
 575         public void addMessageToConsole(String message, int lineNumber,
 576                                         String sourceId)
 577         {
 578             if (!message.isEmpty()) {
 579                 int pos = message.indexOf("file://");
 580                 if (pos != -1) {
 581                     String s1 = message.substring(0, pos);
 582                     String s2 = message.substring(pos);
 583                     try {
 584                         // Extract the last path component aka file name
 585                         s2 = new File(newURL(s2).getPath()).getName();
 586                     } catch (MalformedURLException ignore) {}
 587                     message = s1 + s2;
 588                 }
 589             }
 590             if (lineNumber == 0) {
 591                 out.printf("CONSOLE MESSAGE: %s\n", message);
 592             } else {
 593                 out.printf("CONSOLE MESSAGE: line %d: %s\n",
 594                            lineNumber, message);
 595             }
 596         }
 597 
 598         @Override
 599         public void didClearWindowObject(long context, long windowObject) {
 600             mlog("didClearWindowObject");
 601             DumpRenderTree.didClearWindowObject(context, windowObject,
 602                                                 eventSender);
 603         }
 604     }
 605 }