1 /* 2 * Copyright (c) 2011, 2013, 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 javafx.scene; 27 28 import javafx.collections.ObservableList; 29 import com.sun.javafx.geom.BaseBounds; 30 import com.sun.javafx.geom.transform.BaseTransform; 31 import com.sun.javafx.jmx.MXNodeAlgorithm; 32 import com.sun.javafx.jmx.MXNodeAlgorithmContext; 33 import com.sun.javafx.sg.prism.NGGroup; 34 import com.sun.javafx.sg.prism.NGNode; 35 import org.junit.Rule; 36 import org.junit.Test; 37 import org.junit.rules.ExpectedException; 38 import static org.junit.Assert.assertEquals; 39 import static org.junit.Assert.assertNull; 40 import static org.junit.Assert.assertSame; 41 import static org.junit.Assert.assertTrue; 42 import static org.junit.Assert.fail; 43 44 45 /** 46 * Tests structural aspects of scene graph manipulation. See RT-4095. 47 * 48 * The following notation is used in test names to indicate various 49 * relationships: 50 * CL = clip child 51 * G = Group child 52 * S = Scene child 53 * 54 * Several relationships are checked in each test, typically in 55 * the following order: 56 * 57 * parent's clip 58 * parent's children (for Group and Region) 59 * relationships of second parent (if any) 60 * child's clipParent 61 * child's parent 62 * child's scene 63 */ 64 65 public class StructureTest { 66 @Rule 67 public ExpectedException thrown = ExpectedException.none(); 68 69 // TODO: 70 // - various nasty observableArrayList updates to Group.content and Scene.content. 71 // - various bind expressions. 72 73 ///////////////////////// 74 // Setup and teardown. // 75 ///////////////////////// 76 77 ////////////////////// 78 // Helper Functions // 79 ////////////////////// 80 81 int occurs(Node child, ObservableList<Node> content) { 82 int count = 0; 83 if (content != null) { 84 for (Node node : content) { 85 if (node == child) { 86 count++; 87 } 88 } 89 } 90 return count; 91 } 92 93 /** 94 * Checks whether child occurs exactly once in the content observableArrayList. 95 */ 96 boolean occursOnce(Node child, ObservableList<Node> content) { 97 return 1 == occurs(child, content); 98 } 99 100 /** 101 * Checks whether child does not occur in the content observableArrayList. 102 */ 103 boolean occursZero(Node child, ObservableList<Node> content) { 104 return 0 == occurs(child, content); 105 } 106 107 /** 108 * Checks whether the child node appears exactly once in the 109 * Group's content observableArrayList. 110 */ 111 boolean isChild(Node child, Group group) { 112 return occursOnce(child, group.getChildren()); 113 } 114 115 /** 116 * Checks whether the child node is the root of the Scene. 117 */ 118 boolean isRoot(Parent root, Scene scene) { 119 return root == scene.getRoot(); 120 } 121 122 /** 123 * Checks whether the child node does not appear in the 124 * Group's content observableArrayList. 125 * 126 * We use assertTrue(notChild(child, parent)) instead of 127 * assertFalse(isChild(child, parent)) because we want 128 * to catch cases where child occurs more than once in 129 * the parent's content observableArrayList. 130 */ 131 boolean notChild(Node child, Group group) { 132 return occursZero(child, group.getChildren()); 133 } 134 135 /** 136 * Checks whether the child node does not appear as the root of the 137 * Scene. 138 */ 139 boolean notRoot(Parent root, Scene scene) { 140 return root != scene.getRoot(); 141 } 142 143 ///////////////////////////////////// 144 // Simple Structural Relationships // 145 ///////////////////////////////////// 146 147 @Test 148 public void testOrphan() { 149 Node n = new StubNode(); 150 151 assertNull("clipParent is null", n.getClipParent()); 152 assertNull("parent is null", n.getParent()); 153 assertNull("scene is null", n.getScene()); 154 } 155 156 @Test 157 public void testSimpleCL() { 158 StubNode parent = new StubNode(); 159 StubNode child = new StubNode(); 160 parent.setClip(child); 161 162 assertSame("parent.clip is child", child, parent.getClip()); 163 assertSame("child.clipParent is parent", parent, child.getClipParent()); 164 assertNull("child.parent is null", child.getParent()); 165 assertNull("scene is null", child.getScene()); 166 } 167 168 @Test 169 public void testSimpleG() { 170 StubNode child = new StubNode(); 171 Group group = new Group(child); 172 173 assertNull("group.clip is null", group.getClip()); 174 assertTrue("isChild of group", isChild(child, group)); 175 assertNull("child.clipParent is null", child.getClipParent()); 176 assertSame("child.parent is parent", group, child.getParent()); 177 assertNull("child.getScene() is null", child.getScene()); 178 } 179 180 @Test 181 public void testSimpleS() { 182 StubParent root = new StubParent(); 183 Scene scene = new Scene(root); 184 185 assertTrue("isChild of scene", isRoot(root, scene)); 186 assertNull("child.clipParent is null", root.getClipParent()); 187 assertSame("child.getScene() is scene", scene, root.getScene()); 188 } 189 190 @Test 191 public void testSceneInsertGroup1() { 192 StubNode child = new StubNode(); 193 Group group = new Group(); 194 Scene scene = new Scene(group); 195 group.getChildren().add(child); 196 197 assertSame("group.getScene() is scene", scene, group.getScene()); 198 assertSame("child.getScene() is scene", scene, child.getScene()); 199 } 200 201 @Test 202 public void testSceneInsertGroup2() { 203 StubNode child = new StubNode(); 204 Group group = new Group(); 205 Scene scene = new Scene(group); 206 group.getChildren().add(child); 207 208 209 assertSame("group.getScene() is scene", scene, group.getScene()); 210 assertSame("child.getScene() is scene", scene, child.getScene()); 211 } 212 213 @Test public void testUnparentCL() { 214 StubNode child = new StubNode(); 215 StubNode parent = new StubNode(); 216 parent.setClip(child); 217 parent.setClip(null); 218 219 assertNull("parent.clip is null", parent.getClip()); 220 assertNull("child.clipParent is null", child.getClipParent()); 221 } 222 223 @Test public void testUnparentG() { 224 StubNode child = new StubNode(); 225 Group parent = new Group(child); 226 227 parent.getChildren().remove(child); 228 229 230 assertEquals("parent.content is zero size", 0, parent.getChildren().size()); 231 assertNull("child.parent is null", child.getParent()); 232 } 233 234 //////////////////////////////////// 235 // Illegal Structure Change Tests // 236 //////////////////////////////////// 237 238 // Test attempts to switch from one part of the scene graph to another. 239 // This is the cross product: {CL,CU,G,S}x{CL,CU,G,S} so there 240 // are sixteen cases. 241 242 @Test public void testSwitchCLCL() { 243 StubNode child = new StubNode(); 244 StubNode p1 = new StubNode(); 245 p1.setClip(child); 246 StubNode p2 = new StubNode(); 247 thrown.expect(IllegalArgumentException.class); 248 try { 249 p2.setClip(child); 250 } catch (final IllegalArgumentException e) { 251 assertSame("p1.clip is child", child, p1.getClip()); 252 assertNull("p2.clip is null", p2.getClip()); 253 assertSame("child.clipParent is p1", 254 p1, child.getClipParent()); 255 assertNull("child.parent is null", child.getParent()); 256 assertNull("child.getScene() is null", child.getScene()); 257 throw e; 258 } 259 } 260 261 @Test public void testSwitchCLG() { 262 StubNode child = new StubNode(); 263 StubNode p1 = new StubNode(); 264 p1.setClip(child); 265 Group p2 = new Group(); 266 ObservableList<Node> content = p2.getChildren(); 267 try { 268 content.add(child); 269 fail("IllegalArgument should have been thrown."); 270 } catch (IllegalArgumentException iae) { 271 // expected 272 } 273 274 assertSame("p1.clip is child", child, p1.getClip()); 275 assertNull("p2.clip is null", p2.getClip()); 276 assertTrue("notChild of p2", notChild(child, p2)); 277 assertSame("child.clipParent is p1", p1, child.getClipParent()); 278 assertNull("child.parent is null", child.getParent()); 279 assertNull("child.getScene() is null", child.getScene()); 280 } 281 282 @Test public void testSwitchCLS() { 283 StubParent clipNode = new StubParent(); 284 StubNode p1 = new StubNode(); 285 p1.setClip(clipNode); 286 try { 287 Scene p2 = new Scene(clipNode); 288 fail("IllegalArgument should have been thrown."); 289 } catch (Throwable t) { 290 //expected 291 } 292 assertSame("p1.clip is child", clipNode, p1.getClip()); 293 assertSame("child.clipParent is p1", p1, clipNode.getClipParent()); 294 assertNull("child.parent is null", clipNode.getParent()); 295 assertNull("child.getScene() is null", clipNode.getScene()); 296 } 297 298 @Test public void testSwitchGCL() { 299 StubNode child = new StubNode(); 300 Group p1 = new Group(child); 301 StubNode p2 = new StubNode(); 302 thrown.expect(IllegalArgumentException.class); 303 try { 304 p2.setClip(child); 305 } catch (final IllegalArgumentException e) { 306 assertNull("p1.clip is null", p1.getClip()); 307 assertTrue("isChild of p1", isChild(child, p1)); 308 assertNull("p2.clip is null", p2.getClip()); 309 assertNull("child.clipParent is null", child.getClipParent()); 310 assertSame("child.parent is p1", p1, child.getParent()); 311 assertNull("child.getScene() is null", child.getScene()); 312 throw e; 313 } 314 } 315 316 // TODO XXX TEMPORARY STOPGAP POLICY RT-4095 -- TEST DISABLED 317 318 // @Test public void testSwitchGG() { 319 // var child = new StubNode(); 320 // var p1 = Group { content: [ child ] }; 321 // setHandler(); 322 // var p2 = Group { content: [ child ] }; 323 // 324 // assertTrue("caught IllegalArgumentException", caught instanceof IllegalArgumentException); 325 // assertNull("p1.clip is null", p1.clip); 326 // assertTrue("isChild of p1", isChild(child, p1)); 327 // assertNull("p2.clip is null", p2.clip); 328 // assertTrue("notChild of p2", notChild(child, p2)); 329 // assertNull("child.clipParent is null", child.getClipParent()); 330 // assertSame("child.parent is p1", p1, child.parent); 331 // assertNull("child.getScene() is null", child.getScene()); 332 // } 333 334 // TODO XXX TEMPORARY STOPGAP POLICY RT-4095 -- TEST DISABLED 335 336 // @Test public void testSwitchGS() { 337 // var child = new StubNode(); 338 // var p1 = Group { content: [ child ] }; 339 // setHandler(); 340 // var p2 = Scene { content: [ child ] }; 341 // 342 // assertTrue("caught IllegalArgumentException", caught instanceof IllegalArgumentException); 343 // assertNull("p1.clip is null", p1.clip); 344 // assertTrue("isChild of p1", isChild(child, p1)); 345 // assertTrue("notChild of p2", notChild(child, p2)); 346 // assertNull("child.clipParent is null", child.getClipParent()); 347 // assertSame("child.parent is p1", p1, child.parent); 348 // assertNull("child.getScene() is null", child.getScene()); 349 // } 350 351 // TODO XXX TEMPORARY STOPGAP POLICY RT-4095 -- TEST OF STOPGAP POLICY 352 353 @Test public void testSwitchGGStopgap() { 354 StubNode child = new StubNode(); 355 Group p1 = new Group(child); 356 Group p2 = new Group(child); 357 358 assertTrue("notChild of p1", notChild(child, p1)); 359 assertTrue("isChild of p2", isChild(child, p2)); 360 assertSame("child.parent is p2", p2, child.getParent()); 361 } 362 363 @Test public void testSwitchSCL() { 364 StubParent root = new StubParent(); 365 Scene scene = new Scene(root); 366 StubNode p2 = new StubNode(); 367 thrown.expect(IllegalArgumentException.class); 368 try { 369 p2.setClip(root); 370 } catch (final IllegalArgumentException e) { 371 assertTrue("isRoot of scene", isRoot(root, scene)); 372 assertNull("p2.clip is null", p2.getClip()); 373 assertNull("root.clipParent is null", root.getClipParent()); 374 assertSame("root.getScene() is scene", scene, root.getScene()); 375 throw e; 376 } 377 } 378 379 380 // TODO XXX TEMPORARY STOPGAP POLICY RT-4095 -- TEST DISABLED 381 382 // @Test public void testSwitchSG() { 383 // var child = new StubNode(); 384 // var p1 = Scene { content: [ child ] }; 385 // setHandler(); 386 // var p2 = Group { content: [ child ] }; 387 // 388 // assertTrue("caught IllegalArgumentException", caught instanceof IllegalArgumentException); 389 // assertTrue("isChild of p1", isChild(child, p1)); 390 // assertNull("p2.clip is null", p2.clip); 391 // assertTrue("notChild of p2", notChild(child, p2)); 392 // assertNull("child.clipParent is null", child.getClipParent()); 393 // assertSame("child.parent is p1.impl_root", p1.impl_root, child.parent); 394 // assertSame("child.getScene() is p1", p1, child.getScene()); 395 // } 396 397 // TODO XXX TEMPORARY STOPGAP POLICY RT-4095 -- TEST DISABLED 398 399 // @Test public void testSwitchSS() { 400 // var child = new StubNode(); 401 // var p1 = Scene { content: [ child ] }; 402 // setHandler(); 403 // var p2 = Scene { content: [ child ] }; 404 // 405 // assertTrue("isChild of p1", isChild(child, p1)); 406 // assertTrue("notChild of p2", notChild(child, p2)); 407 // assertNull("child.clipParent is null", child.getClipParent()); 408 // assertSame("child.parent is p1.impl_root", p1.impl_root, child.parent); 409 // assertSame("child.getScene() is p1", p1, child.getScene()); 410 // } 411 412 @Test public void testGroupInsert() { 413 StubNode n0 = new StubNode(); 414 n0.setId("n0"); 415 StubNode n1 = new StubNode(); 416 n1.setId("n1"); 417 StubNode n2 = new StubNode(); 418 n2.setId("n2"); 419 Group g = new Group(n0, n1, n2); 420 421 ObservableList<Node> content = g.getChildren(); 422 try { 423 content.add(n1); 424 fail("IllegalArgument should have been thrown."); 425 } catch (IllegalArgumentException iae) { 426 // expected 427 } 428 429 assertEquals("g.content is size 3", 3, g.getChildren().size()); 430 assertSame("g.content[0] is n0", n0, g.getChildren().get(0)); 431 assertSame("g.content[1] is n1", n1, g.getChildren().get(1)); 432 assertSame("g.content[2] is n2", n2, g.getChildren().get(2)); 433 434 } 435 436 437 @Test public void testGroupReplace1() { 438 StubNode n0 = new StubNode(); 439 n0.setId("n0"); 440 StubNode n1 = new StubNode(); 441 n1.setId("n1"); 442 StubNode n2 = new StubNode(); 443 n2.setId("n2"); 444 Group g = new Group(n0, n1, n2); 445 446 g.getChildren().remove(1); 447 ObservableList<Node> n = javafx.collections.FXCollections.<Node>observableArrayList(); 448 n.addAll(n1,n1); 449 ObservableList<Node> content = g.getChildren(); 450 try { 451 content.addAll(1, n); 452 fail("IllegalArgument should have been thrown."); 453 } catch (IllegalArgumentException iae) { 454 // expected 455 } 456 457 assertEquals("g.content is size 2", 2, g.getChildren().size()); 458 assertSame("g.content[0] is n0", n0, g.getChildren().get(0)); 459 assertSame("g.content[1] is n2", n2, g.getChildren().get(1)); 460 } 461 462 @Test public void testGroupReplace2() { 463 StubNode n0 = new StubNode(); 464 n0.setId("n0"); 465 StubNode n1 = new StubNode(); 466 n1.setId("n1"); 467 StubNode n2 = new StubNode(); 468 n2.setId("n2"); 469 Group g = new Group(n0, n1, n2); 470 471 try { 472 g.getChildren().set(1, n0); 473 fail("No exception thrown."); 474 } catch (IllegalArgumentException e) { 475 //Expected 476 } 477 478 assertEquals("g.content is size 3", 3, g.getChildren().size()); 479 assertSame("g.content[0] is n0", n0, g.getChildren().get(0)); 480 assertSame("g.content[1] is n1", n1, g.getChildren().get(1)); 481 assertSame("g.content[2] is n2", n2, g.getChildren().get(2)); 482 } 483 484 @Test public void testGroupReplace3() { 485 StubNode n0 = new StubNode(); 486 n0.setId("n0"); 487 StubNode n1 = new StubNode(); 488 n1.setId("n1"); 489 StubNode n2 = new StubNode(); 490 n2.setId("n2"); 491 Group g = new Group(n0, n1, n2); 492 g.getChildren().set(1, n1); 493 g.getChildren().set(2, n2); 494 495 assertEquals("g.content is size 3", 3, g.getChildren().size()); 496 assertSame("g.content[0] is n0", n0, g.getChildren().get(0)); 497 assertSame("g.content[1] is n1", n1, g.getChildren().get(1)); 498 assertSame("g.content[2] is n2", n2, g.getChildren().get(2)); 499 } 500 501 /////////////////////// 502 // Circularity Tests // 503 /////////////////////// 504 505 // General form is: given an existing relationship of one kind, add 506 // another relationship of some kind that would cause a circularity. 507 // This is the cross product: {CL,CU,G}x{CL,CU,G}. Also test degenerate 508 // circularities where a node is its own clip, its own child. 509 // The Scene relationship does not occur here, because a Scene cannot 510 // participate in any circular relationship as it has no parent. 511 512 // Test only {CL,G}x{CL,G} for now. 513 514 @Test public void testCircularCLCL() { 515 StubNode node1 = new StubNode(); 516 StubNode node2 = new StubNode(); 517 node2.setClip(node1); 518 thrown.expect(IllegalArgumentException.class); 519 try { 520 node1.setClip(node2); 521 } catch (final IllegalArgumentException e) { 522 assertNull("node1.clip is null", node1.getClip()); 523 assertSame("node1.clipParent is node2", 524 node2, 525 node1.getClipParent()); 526 assertSame("node2.clip is node1", node1, node2.getClip()); 527 assertNull("node2.clipParent is null", node2.getClipParent()); 528 throw e; 529 } 530 } 531 532 @Test public void testCircularCLG() { 533 StubNode node1 = new StubNode(); 534 Group node2 = new Group(node1); 535 thrown.expect(IllegalArgumentException.class); 536 try { 537 node1.setClip(node2); 538 } catch (final IllegalArgumentException e) { 539 assertNull("node1.clip is null", node1.getClip()); 540 assertNull("node1.clipParent is null", node1.getClipParent()); 541 assertSame("node1.parent is node2", node2, node1.getParent()); 542 assertNull("node2.clip is null", node2.getClip()); 543 assertNull("node2.clipParent is null", node2.getClipParent()); 544 assertTrue("node1 is child of node2", isChild(node1, node2)); 545 throw e; 546 } 547 } 548 549 @Test public void testCircularGCL() { 550 Group node1 = new Group(); 551 StubNode node2 = new StubNode(); 552 node2.setClip(node1); 553 554 ObservableList<Node> content = node1.getChildren(); 555 try { 556 content.add(node2); 557 fail("IllegalArgument should have been thrown."); 558 } catch (IllegalArgumentException iae) { 559 // expected 560 } 561 562 assertNull("node1.clip is null", node1.getClip()); 563 assertSame("node1.clipParent is node2", node2, node1.getClipParent()); 564 assertTrue("node2 is not child of node1", notChild(node2, node1)); 565 assertSame("node2.clip is node1", node1, node2.getClip()); 566 assertNull("node2.clipParent is null", node2.getClipParent()); 567 assertNull("node2.parent is null", node2.getParent()); 568 } 569 570 @Test public void testCircularGG() { 571 Group node1 = new Group(); 572 Group node2 = new Group(node1); 573 574 ObservableList<Node> content = node1.getChildren(); 575 try { 576 content.add(node2); 577 fail("IllegalArgument should have been thrown."); 578 } catch (IllegalArgumentException iae) { 579 // expected 580 } 581 582 assertSame("node1.parent is node2", node2, node1.getParent()); 583 assertTrue("node2 is not a child of node1", notChild(node2, node1)); 584 assertNull("node2.parent is null", node2.getParent()); 585 assertTrue("node1 is child of node2", isChild(node1, node2)); 586 } 587 588 @Test public void testCircularSelfCL() { 589 StubNode node1 = new StubNode(); 590 thrown.expect(IllegalArgumentException.class); 591 try { 592 node1.setClip(node1); 593 } catch (final IllegalArgumentException e) { 594 assertNull("node1.clip is null", node1.getClip()); 595 assertNull("node1.clipParent is null", node1.getClipParent()); 596 throw e; 597 } 598 } 599 600 @Test public void testCircularSelfG() { 601 Group node1 = new Group(); 602 603 ObservableList<Node> content = node1.getChildren(); 604 try { 605 content.add(node1); 606 fail("IllegalArgument should have been thrown."); 607 } catch (IllegalArgumentException iae) { 608 // expected 609 } 610 611 assertTrue("node1 is not a child of itself", notChild(node1, node1)); 612 assertNull("node1.parent is null", node1.getParent()); 613 } 614 615 ////////////////////////// 616 // Bound Variable Tests // 617 ////////////////////////// 618 619 // Test various cases where a structure variable (Node.clip, 620 // Group.content, Scene.content) is initialized to a bind-expression. 621 // If the trigger attempts to roll back a change to a variable 622 // initialized this way, the attempt will fail and will throw an 623 // exception. This will leave the invariant violation in place! 624 // We can't do anything about this without language support, so 625 // don't test these cases for now. 626 627 // FAILS: 628 // @Test public void testBindClip() { 629 // var c:Node = null; 630 // var p1 = StubNode { clip: bind c id: "p1" }; 631 // var p2 = StubNode { clip: bind c id: "p2" }; 632 // c = StubNode { id: "c" }; 633 // 634 // println("testBindClip"); 635 // println("p1 = {p1}"); 636 // println("p2 = {p2}"); 637 // println("c = {c}"); 638 // println("p1.clip = {p1.clip}"); 639 // println("p2.clip = {p2.clip}"); 640 // println("c.clipParent = {c.getClipParent()}"); 641 // } 642 643 } 644 645 646 //////////////////// 647 // Helper Classes // 648 //////////////////// 649 650 // 651 // * A stub node that contains as little functionality as possible. 652 // * 653 class StubNode extends Node { 654 655 // * Returning null from impl_createPGNode() causes crashes, 656 // * so return a PGGroup. 657 // * 658 @Override 659 protected NGNode impl_createPeer() { 660 return new NGGroup(); 661 } 662 663 @Override 664 public BaseBounds impl_computeGeomBounds(BaseBounds bounds, BaseTransform tx) { 665 return bounds; 666 } 667 668 @Override 669 public boolean impl_computeContains(double localX, double localY) { 670 // TODO: Missing code. 671 return false; 672 } 673 674 @Override 675 public Object impl_processMXNode(MXNodeAlgorithm alg, MXNodeAlgorithmContext ctx) { 676 return null; 677 } 678 } 679 680 class StubParent extends Parent { 681 682 // * Returning null from impl_createPGNode() causes crashes, 683 // * so return a PGGroup. 684 // * 685 @Override 686 protected NGNode impl_createPeer() { 687 return new NGGroup(); 688 } 689 690 @Override 691 public BaseBounds impl_computeGeomBounds(BaseBounds bounds, BaseTransform tx) { 692 return bounds; 693 } 694 695 @Override 696 public boolean impl_computeContains(double localX, double localY) { 697 // TODO: Missing code. 698 return false; 699 } 700 } 701