1 /*
   2  * Copyright (c) 2010, 2015, 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.oracle.javafx.jmx;
  27 
  28 import java.awt.image.BufferedImage;
  29 import java.io.File;
  30 import java.util.ArrayList;
  31 import java.util.Iterator;
  32 import java.util.LinkedHashMap;
  33 import java.util.List;
  34 import java.util.Map;
  35 import java.util.concurrent.CountDownLatch;
  36 import javax.imageio.ImageIO;
  37 
  38 import javafx.collections.ObservableList;
  39 import javafx.geometry.Bounds;
  40 import javafx.scene.Node;
  41 import javafx.scene.Parent;
  42 import javafx.scene.Scene;
  43 import javafx.scene.image.Image;
  44 import javafx.stage.Window;
  45 import javafx.embed.swing.SwingFXUtils;
  46 
  47 import com.oracle.javafx.jmx.json.JSONDocument;
  48 import javafx.css.CssMetaData;
  49 import javafx.css.Styleable;
  50 import javafx.css.StyleableProperty;
  51 import com.sun.javafx.jmx.HighlightRegion;
  52 import com.sun.javafx.jmx.MXNodeAlgorithm;
  53 import com.sun.javafx.jmx.MXNodeAlgorithmContext;
  54 import com.sun.javafx.scene.SceneHelper;
  55 import com.sun.javafx.stage.WindowHelper;
  56 import com.sun.javafx.tk.TKPulseListener;
  57 import com.sun.javafx.tk.TKScene;
  58 import com.sun.javafx.tk.Toolkit;
  59 import com.sun.media.jfxmedia.AudioClip;
  60 import com.sun.media.jfxmedia.MediaManager;
  61 import com.sun.media.jfxmedia.MediaPlayer;
  62 import com.sun.media.jfxmedia.events.PlayerStateEvent.PlayerState;
  63 
  64 /**
  65  * Default implementation of {@link SGMXBean} interface.
  66  */
  67 public class SGMXBeanImpl implements SGMXBean, MXNodeAlgorithm {
  68 
  69     private static final String SGMX_NOT_PAUSED_TEXT = "Scene-graph is not PAUSED.";
  70     private static final String SGMX_CALL_GETSGTREE_FIRST = "You need to call getSGTree() first.";
  71 
  72     private boolean paused = false;
  73 
  74     private Map<Integer, Window> windowMap = null;
  75     private JSONDocument jwindows = null;
  76     private Map<Integer, Node> nodeMap = null;
  77     private JSONDocument[] jsceneGraphs = null;
  78     private Map<Scene, BufferedImage> scene2Image = null;
  79 
  80     private List<MediaPlayer> playersToResume = null;
  81 
  82     /**
  83      * {@inheritDoc}
  84      */
  85     @Override
  86     public void pause() {
  87         if (paused) {
  88             return;
  89         }
  90         paused = true;
  91         releaseAllStateObject();
  92         Toolkit tk = Toolkit.getToolkit();
  93         tk.pauseScenes();
  94         pauseMedia();
  95     }
  96 
  97     /**
  98      * {@inheritDoc}
  99      */
 100     @Override
 101     public void resume() {
 102         if (!paused) {
 103             return;
 104         }
 105         paused = false;
 106         releaseAllStateObject();
 107         Toolkit tk = Toolkit.getToolkit();
 108         tk.resumeScenes();
 109         resumeMedia();
 110     }
 111 
 112     /**
 113      * {@inheritDoc}
 114      */
 115     @Override
 116     public void step() throws IllegalStateException {
 117         if (!paused) {
 118             throw new IllegalStateException(SGMX_NOT_PAUSED_TEXT);
 119         }
 120 
 121         releaseAllStateObject();
 122 
 123         Toolkit tk = Toolkit.getToolkit();
 124         final CountDownLatch onePulseLatch = new CountDownLatch(1);
 125 
 126         tk.setLastTkPulseListener(new TKPulseListener() {
 127             @Override public void pulse() {
 128                 onePulseLatch.countDown();
 129             }
 130         });
 131 
 132         tk.resumeScenes();
 133 
 134         try {
 135             onePulseLatch.await();
 136         } catch (InterruptedException e) { }
 137 
 138         tk.pauseScenes();
 139         tk.setLastTkPulseListener(null);
 140     }
 141 
 142     /**
 143      * {@inheritDoc}
 144      */
 145     @Override
 146     public String getWindows() throws IllegalStateException {
 147         if (!paused) {
 148             throw new IllegalStateException(SGMX_NOT_PAUSED_TEXT);
 149         }
 150         importWindowsIfNeeded();
 151         return jwindows.toJSON();
 152     }
 153 
 154     /**
 155      * {@inheritDoc}
 156      */
 157     @Override
 158     public String getSGTree(int windowId) throws IllegalStateException {
 159         if (!paused) {
 160             throw new IllegalStateException(SGMX_NOT_PAUSED_TEXT);
 161         }
 162         importWindowsIfNeeded();
 163         if (nodeMap == null) {
 164              nodeMap = new LinkedHashMap<Integer, Node>();
 165         }
 166         if (jsceneGraphs[windowId] == null) {
 167             final Window window = windowMap.get(windowId);
 168             this.importSGTree(window.getScene().getRoot(), windowId);
 169         }
 170         return jsceneGraphs[windowId].toJSON();
 171     }
 172 
 173     /**
 174      * {@inheritDoc}
 175      */
 176     @Override
 177     public void addHighlightedNode(int nodeId) throws IllegalStateException {
 178         if (!paused) {
 179             throw new IllegalStateException(SGMX_NOT_PAUSED_TEXT);
 180         }
 181         Toolkit.getToolkit().getHighlightedRegions().add(
 182                                 createHighlightRegion(nodeId));
 183         SceneHelper.getPeer(getNode(nodeId).getScene()).markDirty();
 184     }
 185 
 186     /**
 187      * {@inheritDoc}
 188      */
 189     @Override
 190     public void removeHighlightedNode(int nodeId) throws IllegalStateException {
 191         if (!paused) {
 192             throw new IllegalStateException(SGMX_NOT_PAUSED_TEXT);
 193         }
 194         Toolkit.getToolkit().getHighlightedRegions().remove(
 195                                 createHighlightRegion(nodeId));
 196         SceneHelper.getPeer(getNode(nodeId).getScene()).markDirty();
 197     }
 198 
 199     /**
 200      * {@inheritDoc}
 201      */
 202     @Override
 203     public void addHighlightedRegion(int windowId, double x, double y, double w, double h)
 204         throws IllegalStateException
 205     {
 206         if (!paused) {
 207             throw new IllegalStateException(SGMX_NOT_PAUSED_TEXT);
 208         }
 209         TKScene scenePeer = SceneHelper.getPeer(getScene(windowId));
 210         Toolkit.getToolkit().getHighlightedRegions().add(
 211                           new HighlightRegion(scenePeer, x, y, w, h));
 212         scenePeer.markDirty();
 213     }
 214 
 215     /**
 216      * {@inheritDoc}
 217      */
 218     @Override
 219     public void removeHighlightedRegion(int windowId, double x, double y, double w, double h)
 220         throws IllegalStateException
 221     {
 222         if (!paused) {
 223             throw new IllegalStateException(SGMX_NOT_PAUSED_TEXT);
 224         }
 225         TKScene scenePeer = SceneHelper.getPeer(getScene(windowId));
 226         Toolkit.getToolkit().getHighlightedRegions().remove(
 227                           new HighlightRegion(scenePeer, x, y, w, h));
 228         scenePeer.markDirty();
 229     }
 230 
 231     private Node getNode(int nodeId) {
 232         if (nodeMap == null) {
 233             throw new IllegalStateException(SGMX_CALL_GETSGTREE_FIRST);
 234         }
 235         Node node = nodeMap.get(nodeId);
 236         if (node == null) {
 237             throw new IllegalArgumentException("Wrong node id.");
 238         }
 239         return node;
 240     }
 241 
 242     private Scene getScene(int windowId) {
 243         if (windowMap == null) {
 244             throw new IllegalStateException(SGMX_CALL_GETSGTREE_FIRST);
 245         }
 246         Window window = windowMap.get(windowId);
 247         if (window == null) {
 248             throw new IllegalArgumentException("Wrong window id.");
 249         }
 250         return window.getScene();
 251     }
 252 
 253     private HighlightRegion createHighlightRegion(int nodeId) {
 254         Node node = getNode(nodeId);
 255         Bounds bounds = node.localToScene(node.getBoundsInLocal());
 256         return new HighlightRegion(SceneHelper.getPeer(node.getScene()),
 257                                    bounds.getMinX(),
 258                                    bounds.getMinY(),
 259                                    bounds.getWidth(),
 260                                    bounds.getHeight());
 261     }
 262 
 263     /**
 264      * {@inheritDoc}
 265      */
 266     @Override
 267     public String makeScreenShot(int nodeId) throws IllegalStateException {
 268         if (!paused) {
 269             throw new IllegalStateException(SGMX_NOT_PAUSED_TEXT);
 270         }
 271         if (nodeMap == null) {
 272             throw new IllegalStateException(SGMX_CALL_GETSGTREE_FIRST);
 273         }
 274 
 275         Node node = nodeMap.get(nodeId);
 276         if (node == null) {
 277             return null;
 278         }
 279 
 280         Scene scene = node.getScene();
 281         Bounds sceneBounds = node.localToScene(node.getBoundsInLocal());
 282         return getScreenShotPath(scene, sceneBounds.getMinX(), sceneBounds.getMinY(),
 283                 sceneBounds.getWidth(), sceneBounds.getHeight());
 284     }
 285 
 286     /**
 287      * {@inheritDoc}
 288      */
 289     @Override
 290     public String makeScreenShot(int windowId, double x, double y, double w, double h)
 291         throws IllegalStateException
 292     {
 293         if (!paused) {
 294             throw new IllegalStateException(SGMX_NOT_PAUSED_TEXT);
 295         }
 296         if (nodeMap == null) {
 297             throw new IllegalStateException(SGMX_CALL_GETSGTREE_FIRST);
 298         }
 299         Scene scene = getScene(windowId);
 300         return getScreenShotPath(scene, x, y, w, h);
 301     }
 302 
 303     private String getScreenShotPath(Scene scene, double x, double y, double w, double h) {
 304         if (scene2Image == null) {
 305             scene2Image = new LinkedHashMap<Scene, BufferedImage>();
 306         }
 307 
 308         BufferedImage bufferedImage = scene2Image.get(scene);
 309         if (bufferedImage == null) {
 310             Image fxImage = scene.snapshot(null);
 311             bufferedImage  = SwingFXUtils.fromFXImage(fxImage, null);
 312             scene2Image.put(scene, bufferedImage);
 313         }
 314 
 315         BufferedImage nodeImage = bufferedImage.getSubimage((int)x, (int)y, (int)w, (int)h);
 316 
 317         File tmpFile = null;
 318         try {
 319             tmpFile = File.createTempFile("jfx", ".png");
 320             ImageIO.write(nodeImage, "PNG", tmpFile);
 321             tmpFile.deleteOnExit();
 322             return tmpFile.getAbsolutePath();
 323         } catch (Exception e) {
 324             e.printStackTrace();
 325         }
 326         return null;
 327     }
 328 
 329     private void releaseAllStateObject() {
 330         clearWindowMap();
 331         jwindows = null;
 332         clearNodeMap();
 333         jsceneGraphs = null;
 334         clearScene2Image();
 335     }
 336 
 337     private void clearWindowMap() {
 338         if (windowMap != null) {
 339             windowMap.clear();
 340             windowMap = null;
 341         }
 342     }
 343 
 344     private void clearNodeMap() {
 345         if (nodeMap != null) {
 346             nodeMap.clear();
 347             nodeMap = null;
 348         }
 349      }
 350 
 351     private void clearScene2Image() {
 352         if (scene2Image != null) {
 353             scene2Image.clear();
 354             scene2Image = null;
 355         }
 356     }
 357 
 358     private void importWindowsIfNeeded() {
 359         if (windowMap == null) {
 360             windowMap = new LinkedHashMap<Integer, Window>();
 361             this.importWindows();
 362         }
 363     }
 364 
 365     private void importWindows() {
 366         int windowCount = 0;
 367         final List<Window> windows = Window.getWindows();
 368 
 369         jwindows = JSONDocument.createArray();
 370         for (Window window : windows) {
 371             windowMap.put(windowCount, window);
 372 
 373             final JSONDocument jwindow = JSONDocument.createObject();
 374             jwindow.setNumber("id", windowCount);
 375             jwindow.setString("type", WindowHelper.getMXWindowType(window));
 376             jwindows.array().add(jwindow);
 377             windowCount++;
 378         }
 379 
 380         jsceneGraphs = new JSONDocument[windowCount];
 381     }
 382 
 383     private void importSGTree(Node sgRoot, int windowId) {
 384         if (sgRoot == null) {
 385             return;
 386         }
 387         jsceneGraphs[windowId] = (JSONDocument)sgRoot.impl_processMXNode(this,
 388             new MXNodeAlgorithmContext(nodeMap.size()));
 389     }
 390 
 391     /**
 392      * {@inheritDoc}
 393      */
 394     @Override
 395     public String getCSSInfo(int nodeId) throws IllegalStateException {
 396         if (!paused) {
 397             throw new IllegalStateException(SGMX_NOT_PAUSED_TEXT);
 398         }
 399         if (nodeMap == null) {
 400             throw new IllegalStateException(SGMX_CALL_GETSGTREE_FIRST);
 401         }
 402 
 403         Node node = nodeMap.get(nodeId);
 404         if (node == null) {
 405             return null;
 406         }
 407 
 408         JSONDocument d = new JSONDocument(JSONDocument.Type.OBJECT);
 409 
 410         List<CssMetaData<? extends Styleable, ?>> styleables = node.getCssMetaData();
 411 
 412         for (CssMetaData sp: styleables) {
 413             processCssMetaData(sp, node, d);
 414         }
 415 
 416         return d.toJSON();
 417     }
 418 
 419     private static void processCssMetaData(CssMetaData sp, Node node, JSONDocument d) {
 420 
 421         List<CssMetaData> subProps = sp.getSubProperties();
 422         if (subProps != null && !subProps.isEmpty()) {
 423             for (CssMetaData subSp: subProps) {
 424                 processCssMetaData(subSp, node, d);
 425             }
 426         }
 427 
 428         try {
 429             StyleableProperty writable = sp.getStyleableProperty(node);
 430             Object value = writable != null ? writable.getValue() : null;
 431             if (value != null) {
 432                 d.setString(sp.getProperty(), value.toString());
 433             } else {
 434                 d.setString(sp.getProperty(), "null");
 435             }
 436         } catch (Exception e) {
 437             System.out.println(e);
 438             e.printStackTrace();
 439         }
 440     }
 441 
 442     private static String upcaseFirstLetter(String s) {
 443         return s.substring(0, 1).toUpperCase() + s.substring(1);
 444     }
 445 
 446     /**
 447      * {@inheritDoc}
 448      */
 449     @Override
 450     public String getBounds(int nodeId) throws IllegalStateException {
 451         if (!paused) {
 452             throw new IllegalStateException(SGMX_NOT_PAUSED_TEXT);
 453         }
 454         if (nodeMap == null) {
 455             throw new IllegalStateException(SGMX_CALL_GETSGTREE_FIRST);
 456         }
 457 
 458         Node node = nodeMap.get(nodeId);
 459         if (node == null) {
 460             return null;
 461         }
 462 
 463         Bounds sceneBounds = node.localToScene(node.getBoundsInLocal());
 464         JSONDocument d = JSONDocument.createObject();
 465         d.setNumber("x", sceneBounds.getMinX());
 466         d.setNumber("y", sceneBounds.getMinY());
 467         d.setNumber("w", sceneBounds.getWidth());
 468         d.setNumber("h", sceneBounds.getHeight());
 469 
 470         return d.toJSON();
 471     }
 472 
 473     /**
 474      * {@inheritDoc}
 475      */
 476     @Override
 477     public Object processLeafNode(Node node, MXNodeAlgorithmContext ctx) {
 478         return createJSONDocument(node, ctx);
 479     }
 480 
 481     /**
 482      * {@inheritDoc}
 483      */
 484     @Override
 485     public Object processContainerNode(Parent parent, MXNodeAlgorithmContext ctx) {
 486         JSONDocument d = createJSONDocument(parent, ctx);
 487 
 488         final ObservableList<Node> children = parent.getChildrenUnmodifiable();
 489 
 490         JSONDocument childrenDoc = JSONDocument.createArray(children.size());
 491         d.set("children", childrenDoc);
 492 
 493         for (int i = 0; i < children.size(); i++) {
 494             childrenDoc.set(i, (JSONDocument)children.get(i).impl_processMXNode(this, ctx));
 495         }
 496         return d;
 497     }
 498 
 499     private JSONDocument createJSONDocument(Node n, MXNodeAlgorithmContext ctx) {
 500         int id = ctx.getNextInt();
 501 
 502         nodeMap.put(id, n);
 503 
 504         JSONDocument d = JSONDocument.createObject();
 505         d.setNumber("id", id);
 506         d.setString("class", n.getClass().getSimpleName());
 507         return d;
 508     }
 509 
 510     private void pauseMedia() {
 511         AudioClip.stopAllClips();
 512 
 513         List<MediaPlayer> allPlayers = MediaManager.getAllMediaPlayers();
 514         if (allPlayers == null) {
 515             return;
 516         }
 517         if ((!allPlayers.isEmpty()) && (playersToResume == null)) {
 518             playersToResume = new ArrayList<MediaPlayer>();
 519         }
 520         for (MediaPlayer player: allPlayers) {
 521             if (player.getState() == PlayerState.PLAYING) {
 522                 player.pause();
 523                 playersToResume.add(player);
 524             }
 525         }
 526     }
 527 
 528     private void resumeMedia() {
 529         if (playersToResume == null) {
 530             return;
 531         }
 532         for (MediaPlayer player: playersToResume) {
 533             player.play();
 534         }
 535         playersToResume.clear();
 536     }
 537 }