/* * Copyright (C) 2011 eXo Platform SAS. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.exoplatform.portal.mop.navigation; import java.util.Iterator; import org.exoplatform.portal.mop.SiteKey; import org.gatein.mop.api.workspace.Navigation; import org.gatein.mop.api.workspace.ObjectType; import org.gatein.mop.api.workspace.Site; import org.gatein.mop.core.api.MOPService; /** * @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a> */ public class TestNavigationServiceUpdate extends AbstractTestNavigationService { public void testNoop() throws Exception { MOPService mop = mgr.getPOMService(); Site portal = mop.getModel().getWorkspace().addSite(ObjectType.PORTAL_SITE, "update_no_op"); Navigation def = portal.getRootNavigation().addChild("default"); def.addChild("a"); def.addChild("b"); def.addChild("c"); def.addChild("d"); // sync(true); // NavigationContext navigation = service.loadNavigation(SiteKey.portal("update_no_op")); NodeContext<Node> root = service.loadNode(Node.MODEL, navigation, Scope.ALL, null); Iterator<NodeChange<Node>> it = root.node.update(service, null); assertFalse(it.hasNext()); } public void testHasChanges() throws Exception { MOPService mop = mgr.getPOMService(); Site portal = mop.getModel().getWorkspace().addSite(ObjectType.PORTAL_SITE, "update_cannot_save"); Navigation def = portal.getRootNavigation().addChild("default"); // sync(true); // NavigationContext navigation = service.loadNavigation(SiteKey.portal("update_cannot_save")); Node root = service.loadNode(Node.MODEL, navigation, Scope.ALL, null).getNode(); // assertFalse(root.context.hasChanges()); root.addChild("foo"); assertTrue(root.context.hasChanges()); // try { root.update(service, null); } catch (IllegalArgumentException expected) { } // assertTrue(root.context.hasChanges()); service.saveNode(root.context, null); assertFalse(root.context.hasChanges()); // Iterator<NodeChange<Node>> it = root.update(service, null); assertFalse(it.hasNext()); } public void testAddFirst() throws Exception { MOPService mop = mgr.getPOMService(); Site portal = mop.getModel().getWorkspace().addSite(ObjectType.PORTAL_SITE, "update_add_first"); portal.getRootNavigation().addChild("default"); // sync(true); // NavigationContext navigation = service.loadNavigation(SiteKey.portal("update_add_first")); NodeContext<Node> root1 = service.loadNode(Node.MODEL, navigation, Scope.ALL, null); assertEquals(0, root1.getNodeSize()); Node root2 = service.loadNode(Node.MODEL, navigation, Scope.ALL, null).getNode(); root2.addChild("a"); service.saveNode(root2.context, null); // sync(true); // root1.node.update(service, null); assertEquals(1, root1.getNodeSize()); Node a = root1.getNode(0); assertEquals("a", a.getName()); } public void testAddSecond() throws Exception { MOPService mop = mgr.getPOMService(); Site portal = mop.getModel().getWorkspace().addSite(ObjectType.PORTAL_SITE, "update_add_second"); portal.getRootNavigation().addChild("default").addChild("a"); // sync(true); // NavigationContext navigation = service.loadNavigation(SiteKey.portal("update_add_second")); Node root1 = service.loadNode(Node.MODEL, navigation, Scope.ALL, null).getNode(); Node a = root1.getChild("a"); assertEquals(1, root1.getSize()); Node root2 = service.loadNode(Node.MODEL, navigation, Scope.ALL, null).getNode(); root2.addChild("b"); service.saveNode(root2.context, null); // sync(true); // Iterator<NodeChange<Node>> changes = root1.update(service, null); NodeChange.Added<Node> added = (NodeChange.Added<Node>) changes.next(); assertSame(root1, added.getParent()); assertSame(root1.getChild("b"), added.getTarget()); assertSame(a, added.getPrevious()); assertFalse(changes.hasNext()); assertEquals(2, root1.getSize()); assertEquals("a", root1.getChild(0).getName()); assertEquals("b", root1.getChild(1).getName()); } public void testRemove() throws Exception { MOPService mop = mgr.getPOMService(); Site portal = mop.getModel().getWorkspace().addSite(ObjectType.PORTAL_SITE, "update_remove"); portal.getRootNavigation().addChild("default").addChild("a"); // sync(true); // NavigationContext navigation = service.loadNavigation(SiteKey.portal("update_remove")); NodeContext<Node> root1 = service.loadNode(Node.MODEL, navigation, Scope.ALL, null); assertEquals(1, root1.getNodeSize()); Node a = root1.getNode("a"); Node root2 = service.loadNode(Node.MODEL, navigation, Scope.ALL, null).getNode(); root2.removeChild("a"); service.saveNode(root2.context, null); // sync(true); // Iterator<NodeChange<Node>> changes = root1.node.update(service, null); NodeChange.Removed<Node> removed = (NodeChange.Removed<Node>) changes.next(); assertSame(root1.node, removed.getParent()); assertSame(a, removed.getTarget()); assertFalse(changes.hasNext()); assertEquals(0, root1.getNodeSize()); } public void testMove() throws Exception { MOPService mop = mgr.getPOMService(); Site portal = mop.getModel().getWorkspace().addSite(ObjectType.PORTAL_SITE, "update_move"); portal.getRootNavigation().addChild("default").addChild("a").addChild("b"); portal.getRootNavigation().getChild("default").addChild("c"); // sync(true); // NavigationContext navigation = service.loadNavigation(SiteKey.portal("update_move")); NodeContext<Node> root1 = service.loadNode(Node.MODEL, navigation, Scope.ALL, null); assertEquals(2, root1.getNodeSize()); Node a = root1.getNode("a"); Node b = a.getChild("b"); Node c = root1.getNode("c"); Node root2 = service.loadNode(Node.MODEL, navigation, Scope.ALL, null).getNode(); root2.getChild("c").addChild(root2.getChild("a").getChild("b")); service.saveNode(root2.context, null); // sync(true); // Iterator<NodeChange<Node>> changes = root1.node.update(service, null); NodeChange.Moved<Node> moved = (NodeChange.Moved<Node>) changes.next(); assertSame(a, moved.getFrom()); assertSame(c, moved.getTo()); assertSame(b, moved.getTarget()); assertSame(null, moved.getPrevious()); assertFalse(changes.hasNext()); assertEquals(0, root1.getNode("a").getSize()); assertEquals(1, root1.getNode("c").getSize()); } public void testAddWithSameName() throws Exception { MOPService mop = mgr.getPOMService(); Site portal = mop.getModel().getWorkspace().addSite(ObjectType.PORTAL_SITE, "update_add_with_same_name"); portal.getRootNavigation().addChild("default"); // sync(true); // NavigationContext navigation = service.loadNavigation(SiteKey.portal("update_add_with_same_name")); Node root1 = service.loadNode(Node.MODEL, navigation, Scope.ALL, null).getNode(); root1.addChild("a").addChild("b"); root1.addChild("c"); service.saveNode(root1.context, null); // sync(true); // root1 = service.loadNode(Node.MODEL, navigation, Scope.ALL, null).getNode(); Node a = root1.getChild("a"); Node b = a.getChild("b"); Node c = root1.getChild("c"); // Node root2 = service.loadNode(Node.MODEL, navigation, Scope.ALL, null).getNode(); root2.getChild("c").addChild(root2.getChild("a").getChild("b")); Node b2 = root2.getChild("a").addChild("b"); service.saveNode(root2.context, null); // Iterator<NodeChange<Node>> changes = root1.update(service, null); NodeChange.Added<Node> added = (NodeChange.Added<Node>) changes.next(); assertNull(added.getPrevious()); assertSame(a, added.getParent()); NodeChange.Moved<Node> moved = (NodeChange.Moved<Node>) changes.next(); assertNull(moved.getPrevious()); assertSame(a, moved.getFrom()); assertSame(c, moved.getTo()); assertSame(b, moved.getTarget()); assertFalse(changes.hasNext()); // assertSame(a, root1.getChild("a")); assertSame(c, root1.getChild("c")); assertSame(b, c.getChild("b")); assertEquals(b2.getId(), a.getChild("b").getId()); assertSame(a.getChild("b"), added.getTarget()); } public void testComplex() throws Exception { MOPService mop = mgr.getPOMService(); Site portal = mop.getModel().getWorkspace().addSite(ObjectType.PORTAL_SITE, "update_complex"); portal.getRootNavigation().addChild("default"); // sync(true); // NavigationContext navigation = service.loadNavigation(SiteKey.portal("update_complex")); Node root1 = service.loadNode(Node.MODEL, navigation, Scope.ALL, null).getNode(); Node a1 = root1.addChild("a"); a1.addChild("c"); a1.addChild("d"); a1.addChild("e"); Node b1 = root1.addChild("b"); b1.addChild("f"); b1.addChild("g"); b1.addChild("h"); service.saveNode(root1.context, null); // sync(true); // root1 = service.loadNode(Node.MODEL, navigation, Scope.ALL, null).getNode(); a1 = root1.getChild("a"); Node c1 = a1.getChild("c"); Node d1 = a1.getChild("d"); Node e1 = a1.getChild("e"); b1 = root1.getChild("b"); Node f1 = b1.getChild("f"); Node g1 = b1.getChild("g"); Node h1 = b1.getChild("h"); // Node root2 = service.loadNode(Node.MODEL, navigation, Scope.ALL, null).getNode(); Node a2 = root2.getChild("a"); a2.removeChild("e"); Node b2 = root2.getChild("b"); b2.addChild(2, a2.getChild("d")); a2.addChild(1, "d"); b2.removeChild("g"); service.saveNode(root2.context, null); // sync(true); // Iterator<NodeChange<Node>> changes = root1.update(service, null); NodeChange.Added<Node> added = (NodeChange.Added<Node>) changes.next(); assertSame(a1, added.getParent()); assertEquals("d", added.getTarget().getName()); assertSame(c1, added.getPrevious()); NodeChange.Removed<Node> removed1 = (NodeChange.Removed<Node>) changes.next(); assertSame(a1, removed1.getParent()); assertSame(e1, removed1.getTarget()); NodeChange.Moved<Node> moved = (NodeChange.Moved<Node>) changes.next(); assertSame(a1, moved.getFrom()); assertSame(b1, moved.getTo()); assertSame(d1, moved.getTarget()); assertSame(f1, moved.getPrevious()); NodeChange.Removed<Node> removed2 = (NodeChange.Removed<Node>) changes.next(); assertSame(b1, removed2.getParent()); assertSame(g1, removed2.getTarget()); assertFalse(changes.hasNext()); // assertSame(a1, root1.getChild("a")); assertSame(b1, root1.getChild("b")); assertEquals(2, a1.getSize()); assertSame(c1, a1.getChild(0)); assertNotNull(a1.getChild(1)); assertEquals("d", a1.getChild(1).getName()); assertFalse(d1.getId().equals(a1.getChild(1).getId())); assertEquals(3, b1.getSize()); assertSame(f1, b1.getChild(0)); assertSame(d1, b1.getChild(1)); assertSame(h1, b1.getChild(2)); } public void testReplaceChild() throws NullPointerException, NavigationServiceException { MOPService mop = mgr.getPOMService(); Site portal = mop.getModel().getWorkspace().addSite(ObjectType.PORTAL_SITE, "update_replace_child"); portal.getRootNavigation().addChild("default").addChild("foo"); sync(true); // NavigationContext navigation = service.loadNavigation(SiteKey.portal("update_replace_child")); Node root1 = service.loadNode(Node.MODEL, navigation, Scope.CHILDREN, null).getNode(); String foo1Id = root1.getChild("foo").getId(); // Node root2 = service.loadNode(Node.MODEL, navigation, Scope.CHILDREN, null).getNode(); root2.removeChild("foo"); Node foo = root2.addChild("foo"); foo.setState(new NodeState.Builder().label("foo2").build()); service.saveNode(root2.context, null); String foo2Id = foo.getId(); sync(true); // Iterator<NodeChange<Node>> changes = root1.update(service, null); NodeChange.Added<Node> added = (NodeChange.Added<Node>) changes.next(); assertEquals(foo2Id, added.getTarget().getId()); NodeChange.Removed<Node> removed = (NodeChange.Removed<Node>) changes.next(); assertEquals(foo1Id, removed.getTarget().getId()); assertFalse(changes.hasNext()); // foo = root1.getChild("foo"); assertEquals(foo2Id, foo.getId()); assertEquals("foo2", root1.getChild("foo").getState().getLabel()); } public void testRename() throws NullPointerException, NavigationServiceException { MOPService mop = mgr.getPOMService(); Site portal = mop.getModel().getWorkspace().addSite(ObjectType.PORTAL_SITE, "update_rename"); portal.getRootNavigation().addChild("default").addChild("foo"); sync(true); // NavigationContext navigation = service.loadNavigation(SiteKey.portal("update_rename")); Node root1 = service.loadNode(Node.MODEL, navigation, Scope.CHILDREN, null).getNode(); // Node root2 = service.loadNode(Node.MODEL, navigation, Scope.CHILDREN, null).getNode(); root2.getChild("foo").setName("bar"); service.saveNode(root2.context, null); sync(true); // Iterator<NodeChange<Node>> it = root1.update(service, null); Node bar = root1.getChild(0); assertEquals("bar", bar.getName()); NodeChange.Renamed<Node> renamed = (NodeChange.Renamed<Node>) it.next(); assertEquals("bar", renamed.getName()); assertSame(bar, renamed.getTarget()); } public void testState() throws NullPointerException, NavigationServiceException { MOPService mop = mgr.getPOMService(); Site portal = mop.getModel().getWorkspace().addSite(ObjectType.PORTAL_SITE, "update_state"); portal.getRootNavigation().addChild("default").addChild("foo").addChild("bar"); sync(true); // NavigationContext navigation = service.loadNavigation(SiteKey.portal("update_state")); Node root1 = service.loadNode(Node.MODEL, navigation, Scope.CHILDREN, null).getNode(); Node root2 = service.loadNode(Node.MODEL, navigation, Scope.CHILDREN, null).getNode(); Node root3 = service.loadNode(Node.MODEL, navigation, Scope.GRANDCHILDREN, null).getNode(); // Node root = service.loadNode(Node.MODEL, navigation, Scope.CHILDREN, null).getNode(); root.getChild("foo").setState(new NodeState.Builder().label("foo").build()); service.saveNode(root.context, null); sync(true); // Iterator<NodeChange<Node>> changes = root1.update(service, Scope.GRANDCHILDREN); Node foo = root1.getChild("foo"); assertEquals("foo", foo.getState().getLabel()); NodeChange.Added<Node> added = (NodeChange.Added<Node>) changes.next(); assertEquals("bar", added.getTarget().getName()); assertEquals(null, added.previous); assertEquals("bar", added.target.getName()); NodeChange.Updated<Node> updated = (NodeChange.Updated<Node>) changes.next(); assertSame(foo, updated.getTarget()); assertEquals(new NodeState.Builder().label("foo").build(), updated.getState()); assertFalse(changes.hasNext()); // changes = root2.update(service, null); foo = root2.getChild("foo"); assertEquals("foo", foo.getState().getLabel()); updated = (NodeChange.Updated<Node>) changes.next(); assertSame(foo, updated.getTarget()); assertEquals(new NodeState.Builder().label("foo").build(), updated.getState()); assertFalse(changes.hasNext()); // changes = root3.update(service, null); foo = root3.getChild("foo"); assertEquals("foo", foo.getState().getLabel()); updated = (NodeChange.Updated<Node>) changes.next(); assertSame(foo, updated.getTarget()); assertEquals(new NodeState.Builder().label("foo").build(), updated.getState()); assertFalse(changes.hasNext()); } public void testUseMostActualChildren() throws NullPointerException, NavigationServiceException { MOPService mop = mgr.getPOMService(); Site portal = mop.getModel().getWorkspace().addSite(ObjectType.PORTAL_SITE, "update_with_most_actual_children"); portal.getRootNavigation().addChild("default").addChild("foo").addChild("bar"); sync(true); // NavigationContext navigation = service.loadNavigation(SiteKey.portal("update_with_most_actual_children")); Node root = service.loadNode(Node.MODEL, navigation, Scope.CHILDREN, null).getNode(); Node foo = root.getChild("foo"); sync(true); // Node root1 = service.loadNode(Node.MODEL, navigation, Scope.ALL, null).getNode(); root1.getChild("foo").removeChild("bar"); service.saveNode(root1.context, null); sync(true); // foo.update(service, Scope.CHILDREN); assertNull(foo.getChild("bar")); // Update a second time (it actually test a previous bug) foo.update(service, Scope.CHILDREN); assertNull(foo.getChild("bar")); } public void testUpdateDeletedNode() throws NullPointerException, NavigationServiceException { MOPService mop = mgr.getPOMService(); Site portal = mop.getModel().getWorkspace().addSite(ObjectType.PORTAL_SITE, "update_deleted_node"); portal.getRootNavigation().addChild("default").addChild("foo").addChild("bar"); sync(true); // NavigationContext navigation = service.loadNavigation(SiteKey.portal("update_deleted_node")); Node root = service.loadNode(Node.MODEL, navigation, Scope.ALL, null).getNode(); Node bar = root.getChild("foo").getChild("bar"); sync(true); // Node root1 = service.loadNode(Node.MODEL, navigation, Scope.ALL, null).getNode(); root1.getChild("foo").removeChild("bar"); service.saveNode(root1.context, null); sync(true); // Iterator<NodeChange<Node>> changes = bar.update(service, Scope.CHILDREN); NodeChange.Removed<Node> removed = (NodeChange.Removed<Node>) changes.next(); assertSame(bar, removed.getTarget()); assertFalse(changes.hasNext()); } public void testLoadEvents() throws Exception { MOPService mop = mgr.getPOMService(); Site portal = mop.getModel().getWorkspace().addSite(ObjectType.PORTAL_SITE, "update_load_events"); Navigation fooNav = portal.getRootNavigation().addChild("default").addChild("foo"); fooNav.addChild("bar1"); fooNav.addChild("bar2"); sync(true); // NavigationContext navigation = service.loadNavigation(SiteKey.portal("update_load_events")); Node root = service.loadNode(Node.MODEL, navigation, Scope.SINGLE, null).getNode(); // Iterator<NodeChange<Node>> changes = root.update(service, Scope.ALL); // Node foo = root.getChild(0); assertEquals("foo", foo.getName()); Node bar1 = foo.getChild(0); assertEquals("bar1", bar1.getName()); Node bar2 = foo.getChild(1); assertEquals("bar2", bar2.getName()); // NodeChange.Added<Node> added1 = (NodeChange.Added<Node>) changes.next(); assertSame(foo, added1.getTarget()); NodeChange.Added<Node> added2 = (NodeChange.Added<Node>) changes.next(); assertSame(bar1, added2.getTarget()); NodeChange.Added<Node> added3 = (NodeChange.Added<Node>) changes.next(); assertSame(bar2, added3.getTarget()); assertFalse(changes.hasNext()); } public void testUpdateTwice2() throws NullPointerException, NavigationServiceException { MOPService mop = mgr.getPOMService(); Site portal = mop.getModel().getWorkspace().addSite(ObjectType.PORTAL_SITE, "update_twice2"); portal.getRootNavigation().addChild("default").addChild("foo").addChild("bar"); sync(true); // Browser 1 : Expand the "foo" node NavigationContext navigation = service.loadNavigation(SiteKey.portal("update_twice2")); Node root = service.loadNode(Node.MODEL, navigation, Scope.CHILDREN, null).getNode(); Node foo = root.getChild("foo"); // If this line is commented, the test is passed service.updateNode(foo.context, Scope.CHILDREN, null); sync(true); // Browser 2: Change the "foo" node Node root1 = service.loadNode(Node.MODEL, navigation, Scope.ALL, null).getNode(); root1.getChild("foo").removeChild("bar"); service.saveNode(root1.context, null); sync(true); // Browser 1: Try to expand the "foo" node 2 times ---> NPE after the 2nd updateNode method service.updateNode(foo.context, Scope.CHILDREN, null); service.updateNode(foo.context, Scope.CHILDREN, null); } public void testMove2() throws Exception { MOPService mop = mgr.getPOMService(); Site portal = mop.getModel().getWorkspace().addSite(ObjectType.PORTAL_SITE, "update_move2"); portal.getRootNavigation().addChild("default").addChild("a").addChild("b"); portal.getRootNavigation().getChild("default").addChild("c"); // sync(true); // NavigationContext navigation = service.loadNavigation(SiteKey.portal("update_move2")); NodeContext<Node> root = service.loadNode(Node.MODEL, navigation, Scope.ALL, null); Node a = root.getNode("a"); Node b = a.getChild("b"); Node c = root.getNode("c"); // Browser 2 : move the node "b" from "a" to "c" NodeContext<Node> root1 = service.loadNode(Node.MODEL, navigation, Scope.ALL, null); root1.getNode("c").addChild(root1.getNode("a").getChild("b")); service.saveNode(root1.getNode().context, null); // sync(true); // Browser 1: need NodeChange event to update UI NodeChangeQueue<NodeContext<Node>> queue = new NodeChangeQueue<NodeContext<Node>>(); // If update "root1" --> NodeChange.Moved --> ok // If update "b" --> NodeChange.Add --> ok // update "a" --> no NodeChange, we need an event here (NodeChange.Remove) so UI can be updated service.updateNode(a.context, Scope.CHILDREN, queue); Iterator<NodeChange<NodeContext<Node>>> changes = queue.iterator(); assertTrue(changes.hasNext()); } public void testScope() throws Exception { MOPService mop = mgr.getPOMService(); Site portal = mop.getModel().getWorkspace().addSite(ObjectType.PORTAL_SITE, "update_scope"); portal.getRootNavigation().addChild("default").addChild("a").addChild("b"); portal.getRootNavigation().getChild("default").addChild("c").addChild("d"); // sync(true); // NavigationContext navigation = service.loadNavigation(SiteKey.portal("update_scope")); Node root1 = service.loadNode(Node.MODEL, navigation, Scope.CHILDREN, null).node; Node a = root1.getChild("a"); Node c = root1.getChild("c"); assertFalse(a.context.isExpanded()); assertFalse(c.context.isExpanded()); // Node root2 = service.loadNode(Node.MODEL, navigation, Scope.ALL, null).node; root2.addChild("e"); service.saveNode(root2.context, null); // sync(true); // service.updateNode(a.context, Scope.CHILDREN, null); assertSame(a, root1.getChild("a")); assertSame(c, root1.getChild("c")); assertNotNull(root1.getChild("e")); assertTrue(a.context.isExpanded()); assertFalse(c.context.isExpanded()); assertNotNull(a.getChild("b")); } public void _testPendingChange() throws NullPointerException, NavigationServiceException { MOPService mop = mgr.getPOMService(); Site portal = mop.getModel().getWorkspace().addSite(ObjectType.PORTAL_SITE, "update_pending_change"); Navigation def = portal.getRootNavigation().addChild("default"); def.addChild("foo"); def.addChild("bar"); sync(true); NavigationContext navigation = service.loadNavigation(SiteKey.portal("update_pending_change")); Node root = service.loadNode(Node.MODEL, navigation, Scope.CHILDREN, null).getNode(); Node foo = root.getChild("foo"); Node bar = root.getChild("bar"); // Expand and change the "bar" node service.updateNode(bar.context, Scope.CHILDREN, null); bar.addChild("juu"); // ---> IllegalArgumentException // Can't expand the "foo" node, even it doesn't have any pending changes service.updateNode(foo.context, Scope.CHILDREN, null); } public void testRemovedNavigation() throws Exception { MOPService mop = mgr.getPOMService(); Site portal = mop.getModel().getWorkspace().addSite(ObjectType.PORTAL_SITE, "update_removed_navigation"); portal.getRootNavigation().addChild("default"); // sync(true); // NavigationContext navigation = service.loadNavigation(SiteKey.portal("update_removed_navigation")); Node root = service.loadNode(Node.MODEL, navigation, Scope.ALL, null).getNode(); service.destroyNavigation(navigation); // sync(true); // try { service.updateNode(root.context, null, null); } catch (NavigationServiceException e) { assertSame(NavigationError.UPDATE_CONCURRENTLY_REMOVED_NODE, e.getError()); } } }