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