PageRenderTime 44ms CodeModel.GetById 17ms app.highlight 19ms RepoModel.GetById 0ms app.codeStats 1ms

/libraries/joomla/table/nested.php

https://bitbucket.org/eternaware/joomus
PHP | 1606 lines | 894 code | 198 blank | 514 comment | 103 complexity | 05d12d6e620248760a9bc5ad4480ea11 MD5 | raw file
Possible License(s): LGPL-2.1
   1<?php
   2/**
   3 * @package     Joomla.Platform
   4 * @subpackage  Table
   5 *
   6 * @copyright   Copyright (C) 2005 - 2012 Open Source Matters, Inc. All rights reserved.
   7 * @license     GNU General Public License version 2 or later; see LICENSE
   8 */
   9
  10defined('JPATH_PLATFORM') or die;
  11
  12/**
  13 * Table class supporting modified pre-order tree traversal behavior.
  14 *
  15 * @package     Joomla.Platform
  16 * @subpackage  Table
  17 * @link        http://docs.joomla.org/JTableNested
  18 * @since       11.1
  19 */
  20class JTableNested extends JTable
  21{
  22	/**
  23	 * Object property holding the primary key of the parent node.  Provides
  24	 * adjacency list data for nodes.
  25	 *
  26	 * @var    integer
  27	 * @since  11.1
  28	 */
  29	public $parent_id;
  30
  31	/**
  32	 * Object property holding the depth level of the node in the tree.
  33	 *
  34	 * @var    integer
  35	 * @since  11.1
  36	 */
  37	public $level;
  38
  39	/**
  40	 * Object property holding the left value of the node for managing its
  41	 * placement in the nested sets tree.
  42	 *
  43	 * @var    integer
  44	 * @since  11.1
  45	 */
  46	public $lft;
  47
  48	/**
  49	 * Object property holding the right value of the node for managing its
  50	 * placement in the nested sets tree.
  51	 *
  52	 * @var    integer
  53	 * @since  11.1
  54	 */
  55	public $rgt;
  56
  57	/**
  58	 * Object property holding the alias of this node used to constuct the
  59	 * full text path, forward-slash delimited.
  60	 *
  61	 * @var    string
  62	 * @since  11.1
  63	 */
  64	public $alias;
  65
  66	/**
  67	 * Object property to hold the location type to use when storing the row.
  68	 * Possible values are: ['before', 'after', 'first-child', 'last-child'].
  69	 *
  70	 * @var    string
  71	 * @since  11.1
  72	 */
  73	protected $_location;
  74
  75	/**
  76	 * Object property to hold the primary key of the location reference node to
  77	 * use when storing the row.  A combination of location type and reference
  78	 * node describes where to store the current node in the tree.
  79	 *
  80	 * @var    integer
  81	 * @since  11.1
  82	 */
  83	protected $_location_id;
  84
  85	/**
  86	 * An array to cache values in recursive processes.
  87	 *
  88	 * @var    array
  89	 * @since  11.1
  90	 */
  91	protected $_cache = array();
  92
  93	/**
  94	 * Debug level
  95	 *
  96	 * @var    integer
  97	 * @since  11.1
  98	 */
  99	protected $_debug = 0;
 100
 101	/**
 102	 * Sets the debug level on or off
 103	 *
 104	 * @param   integer  $level  0 = off, 1 = on
 105	 *
 106	 * @return  void
 107	 *
 108	 * @since   11.1
 109	 */
 110	public function debug($level)
 111	{
 112		$this->_debug = (int) $level;
 113	}
 114
 115	/**
 116	 * Method to get an array of nodes from a given node to its root.
 117	 *
 118	 * @param   integer  $pk          Primary key of the node for which to get the path.
 119	 * @param   boolean  $diagnostic  Only select diagnostic data for the nested sets.
 120	 *
 121	 * @return  mixed    An array of node objects including the start node.
 122	 *
 123	 * @since   11.1
 124	 * @throws  RuntimeException on database error
 125	 */
 126	public function getPath($pk = null, $diagnostic = false)
 127	{
 128		$k = $this->_tbl_key;
 129		$pk = (is_null($pk)) ? $this->$k : $pk;
 130
 131		// Get the path from the node to the root.
 132		$query = $this->_db->getQuery(true);
 133		$select = ($diagnostic) ? 'p.' . $k . ', p.parent_id, p.level, p.lft, p.rgt' : 'p.*';
 134		$query->select($select)
 135			->from($this->_tbl . ' AS n, ' . $this->_tbl . ' AS p')
 136			->where('n.lft BETWEEN p.lft AND p.rgt')
 137			->where('n.' . $k . ' = ' . (int) $pk)
 138			->order('p.lft');
 139
 140		$this->_db->setQuery($query);
 141
 142		return $this->_db->loadObjectList();
 143	}
 144
 145	/**
 146	 * Method to get a node and all its child nodes.
 147	 *
 148	 * @param   integer  $pk          Primary key of the node for which to get the tree.
 149	 * @param   boolean  $diagnostic  Only select diagnostic data for the nested sets.
 150	 *
 151	 * @return  mixed    Boolean false on failure or array of node objects on success.
 152	 *
 153	 * @since   11.1
 154	 * @throws  RuntimeException on database error.
 155	 */
 156	public function getTree($pk = null, $diagnostic = false)
 157	{
 158		$k = $this->_tbl_key;
 159		$pk = (is_null($pk)) ? $this->$k : $pk;
 160
 161		// Get the node and children as a tree.
 162		$query = $this->_db->getQuery(true);
 163		$select = ($diagnostic) ? 'n.' . $k . ', n.parent_id, n.level, n.lft, n.rgt' : 'n.*';
 164		$query->select($select)
 165			->from($this->_tbl . ' AS n, ' . $this->_tbl . ' AS p')
 166			->where('n.lft BETWEEN p.lft AND p.rgt')
 167			->where('p.' . $k . ' = ' . (int) $pk)
 168			->order('n.lft');
 169
 170		return $this->_db->setQuery($query)->loadObjectList();
 171	}
 172
 173	/**
 174	 * Method to determine if a node is a leaf node in the tree (has no children).
 175	 *
 176	 * @param   integer  $pk  Primary key of the node to check.
 177	 *
 178	 * @return  boolean  True if a leaf node, false if not or null if the node does not exist.
 179	 *
 180	 * @note    Since 12.1 this method returns null if the node does not exist.
 181	 * @since   11.1
 182	 * @throws  RuntimeException on database error.
 183	 */
 184	public function isLeaf($pk = null)
 185	{
 186		$k = $this->_tbl_key;
 187		$pk = (is_null($pk)) ? $this->$k : $pk;
 188		$node = $this->_getNode($pk);
 189
 190		// Get the node by primary key.
 191		if (empty($node))
 192		{
 193			// Error message set in getNode method.
 194			return null;
 195		}
 196
 197		// The node is a leaf node.
 198		return (($node->rgt - $node->lft) == 1);
 199	}
 200
 201	/**
 202	 * Method to set the location of a node in the tree object.  This method does not
 203	 * save the new location to the database, but will set it in the object so
 204	 * that when the node is stored it will be stored in the new location.
 205	 *
 206	 * @param   integer  $referenceId  The primary key of the node to reference new location by.
 207	 * @param   string   $position     Location type string. ['before', 'after', 'first-child', 'last-child']
 208	 *
 209	 * @return  void
 210	 *
 211	 * @note    Since 12.1 this method returns void and throws an InvalidArgumentException when an invalid position is passed.
 212	 * @since   11.1
 213	 * @throws  InvalidArgumentException
 214	 */
 215	public function setLocation($referenceId, $position = 'after')
 216	{
 217		// Make sure the location is valid.
 218		if (($position != 'before') && ($position != 'after') && ($position != 'first-child') && ($position != 'last-child'))
 219		{
 220			throw new InvalidArgumentException(sprintf('%s::setLocation(%d, *%s*)', get_class($this), $referenceId, $position));
 221		}
 222
 223		// Set the location properties.
 224		$this->_location = $position;
 225		$this->_location_id = $referenceId;
 226	}
 227
 228	/**
 229	 * Method to move a row in the ordering sequence of a group of rows defined by an SQL WHERE clause.
 230	 * Negative numbers move the row up in the sequence and positive numbers move it down.
 231	 *
 232	 * @param   integer  $delta  The direction and magnitude to move the row in the ordering sequence.
 233	 * @param   string   $where  WHERE clause to use for limiting the selection of rows to compact the
 234	 * ordering values.
 235	 *
 236	 * @return  mixed    Boolean true on success.
 237	 *
 238	 * @link    http://docs.joomla.org/JTable/move
 239	 * @since   11.1
 240	 */
 241	public function move($delta, $where = '')
 242	{
 243		$k = $this->_tbl_key;
 244		$pk = $this->$k;
 245
 246		$query = $this->_db->getQuery(true);
 247		$query->select($k);
 248		$query->from($this->_tbl);
 249		$query->where('parent_id = ' . $this->parent_id);
 250		if ($where)
 251		{
 252			$query->where($where);
 253		}
 254		$position = 'after';
 255		if ($delta > 0)
 256		{
 257			$query->where('rgt > ' . $this->rgt);
 258			$query->order('rgt ASC');
 259			$position = 'after';
 260		}
 261		else
 262		{
 263			$query->where('lft < ' . $this->lft);
 264			$query->order('lft DESC');
 265			$position = 'before';
 266		}
 267
 268		$this->_db->setQuery($query);
 269		$referenceId = $this->_db->loadResult();
 270
 271		if ($referenceId)
 272		{
 273			return $this->moveByReference($referenceId, $position, $pk);
 274		}
 275		else
 276		{
 277			return false;
 278		}
 279	}
 280
 281	/**
 282	 * Method to move a node and its children to a new location in the tree.
 283	 *
 284	 * @param   integer  $referenceId  The primary key of the node to reference new location by.
 285	 * @param   string   $position     Location type string. ['before', 'after', 'first-child', 'last-child']
 286	 * @param   integer  $pk           The primary key of the node to move.
 287	 *
 288	 * @return  boolean  True on success.
 289	 *
 290	 * @link    http://docs.joomla.org/JTableNested/moveByReference
 291	 * @since   11.1
 292	 * @throws  RuntimeException on database error.
 293	 */
 294
 295	public function moveByReference($referenceId, $position = 'after', $pk = null)
 296	{
 297		// @codeCoverageIgnoreStart
 298		if ($this->_debug)
 299		{
 300			echo "\nMoving ReferenceId:$referenceId, Position:$position, PK:$pk";
 301		}
 302		// @codeCoverageIgnoreEnd
 303
 304		$k = $this->_tbl_key;
 305		$pk = (is_null($pk)) ? $this->$k : $pk;
 306
 307		// Get the node by id.
 308		if (!$node = $this->_getNode($pk))
 309		{
 310			// Error message set in getNode method.
 311			return false;
 312		}
 313
 314		// Get the ids of child nodes.
 315		$query = $this->_db->getQuery(true);
 316		$query->select($k)
 317			->from($this->_tbl)
 318			->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
 319
 320		$children = $this->_db->setQuery($query)->loadColumn();
 321
 322		// @codeCoverageIgnoreStart
 323		if ($this->_debug)
 324		{
 325			$this->_logtable(false);
 326		}
 327		// @codeCoverageIgnoreEnd
 328
 329		// Cannot move the node to be a child of itself.
 330		if (in_array($referenceId, $children))
 331		{
 332			$e = new UnexpectedValueException(
 333				sprintf('%s::moveByReference(%d, %s, %d) parenting to child.', get_class($this), $referenceId, $position, $pk)
 334			);
 335			$this->setError($e);
 336			return false;
 337		}
 338
 339		// Lock the table for writing.
 340		if (!$this->_lock())
 341		{
 342			return false;
 343		}
 344
 345		/*
 346		 * Move the sub-tree out of the nested sets by negating its left and right values.
 347		 */
 348		$query = $this->_db->getQuery(true);
 349		$query->update($this->_tbl)
 350			->set('lft = lft * (-1), rgt = rgt * (-1)')
 351			->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
 352		$this->_db->setQuery($query);
 353
 354		$this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED');
 355
 356		/*
 357		 * Close the hole in the tree that was opened by removing the sub-tree from the nested sets.
 358		 */
 359		// Compress the left values.
 360		$query = $this->_db->getQuery(true);
 361		$query->update($this->_tbl)
 362			->set('lft = lft - ' . (int) $node->width)
 363			->where('lft > ' . (int) $node->rgt);
 364		$this->_db->setQuery($query);
 365
 366		$this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED');
 367
 368		// Compress the right values.
 369		$query = $this->_db->getQuery(true);
 370		$query->update($this->_tbl)
 371			->set('rgt = rgt - ' . (int) $node->width)
 372			->where('rgt > ' . (int) $node->rgt);
 373		$this->_db->setQuery($query);
 374
 375		$this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED');
 376
 377		// We are moving the tree relative to a reference node.
 378		if ($referenceId)
 379		{
 380			// Get the reference node by primary key.
 381			if (!$reference = $this->_getNode($referenceId))
 382			{
 383				// Error message set in getNode method.
 384				$this->_unlock();
 385				return false;
 386			}
 387
 388			// Get the reposition data for shifting the tree and re-inserting the node.
 389			if (!$repositionData = $this->_getTreeRepositionData($reference, $node->width, $position))
 390			{
 391				// Error message set in getNode method.
 392				$this->_unlock();
 393				return false;
 394			}
 395		}
 396		// We are moving the tree to be the last child of the root node
 397		else
 398		{
 399			// Get the last root node as the reference node.
 400			$query = $this->_db->getQuery(true);
 401			$query->select($this->_tbl_key . ', parent_id, level, lft, rgt')
 402				->from($this->_tbl)
 403				->where('parent_id = 0')
 404				->order('lft DESC');
 405			$this->_db->setQuery($query, 0, 1);
 406			$reference = $this->_db->loadObject();
 407
 408			// @codeCoverageIgnoreStart
 409			if ($this->_debug)
 410			{
 411				$this->_logtable(false);
 412			}
 413			// @codeCoverageIgnoreEnd
 414
 415			// Get the reposition data for re-inserting the node after the found root.
 416			if (!$repositionData = $this->_getTreeRepositionData($reference, $node->width, 'last-child'))
 417			{
 418				// Error message set in getNode method.
 419				$this->_unlock();
 420				return false;
 421			}
 422		}
 423
 424		/*
 425		 * Create space in the nested sets at the new location for the moved sub-tree.
 426		 */
 427		// Shift left values.
 428		$query = $this->_db->getQuery(true);
 429		$query->update($this->_tbl)
 430			->set('lft = lft + ' . (int) $node->width)
 431			->where($repositionData->left_where);
 432		$this->_db->setQuery($query);
 433
 434		$this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED');
 435
 436		// Shift right values.
 437		$query = $this->_db->getQuery(true);
 438		$query->update($this->_tbl)
 439			->set('rgt = rgt + ' . (int) $node->width)
 440			->where($repositionData->right_where);
 441		$this->_db->setQuery($query);
 442
 443		$this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED');
 444
 445		/*
 446		 * Calculate the offset between where the node used to be in the tree and
 447		 * where it needs to be in the tree for left ids (also works for right ids).
 448		 */
 449		$offset = $repositionData->new_lft - $node->lft;
 450		$levelOffset = $repositionData->new_level - $node->level;
 451
 452		// Move the nodes back into position in the tree using the calculated offsets.
 453		$query = $this->_db->getQuery(true);
 454		$query->update($this->_tbl)
 455			->set('rgt = ' . (int) $offset . ' - rgt')
 456			->set('lft = ' . (int) $offset . ' - lft')
 457			->set('level = level + ' . (int) $levelOffset)
 458			->where('lft < 0');
 459		$this->_db->setQuery($query);
 460
 461		$this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED');
 462
 463		// Set the correct parent id for the moved node if required.
 464		if ($node->parent_id != $repositionData->new_parent_id)
 465		{
 466			$query = $this->_db->getQuery(true);
 467			$query->update($this->_tbl);
 468
 469			// Update the title and alias fields if they exist for the table.
 470			if (property_exists($this, 'title') && $this->title !== null)
 471			{
 472				$query->set('title = ' . $this->_db->Quote($this->title));
 473			}
 474			if (property_exists($this, 'alias') && $this->alias !== null)
 475			{
 476				$query->set('alias = ' . $this->_db->Quote($this->alias));
 477			}
 478
 479			$query->set('parent_id = ' . (int) $repositionData->new_parent_id)
 480				->where($this->_tbl_key . ' = ' . (int) $node->$k);
 481			$this->_db->setQuery($query);
 482
 483			$this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED');
 484		}
 485
 486		// Unlock the table for writing.
 487		$this->_unlock();
 488
 489		// Set the object values.
 490		$this->parent_id = $repositionData->new_parent_id;
 491		$this->level = $repositionData->new_level;
 492		$this->lft = $repositionData->new_lft;
 493		$this->rgt = $repositionData->new_rgt;
 494
 495		return true;
 496	}
 497
 498	/**
 499	 * Method to delete a node and, optionally, its child nodes from the table.
 500	 *
 501	 * @param   integer  $pk        The primary key of the node to delete.
 502	 * @param   boolean  $children  True to delete child nodes, false to move them up a level.
 503	 *
 504	 * @return  boolean  True on success.
 505	 *
 506	 * @since   11.1
 507	 */
 508	public function delete($pk = null, $children = true)
 509	{
 510		$k = $this->_tbl_key;
 511		$pk = (is_null($pk)) ? $this->$k : $pk;
 512
 513		// Lock the table for writing.
 514		if (!$this->_lock())
 515		{
 516			// Error message set in lock method.
 517			return false;
 518		}
 519
 520		// If tracking assets, remove the asset first.
 521		if ($this->_trackAssets)
 522		{
 523			$name = $this->_getAssetName();
 524			$asset = JTable::getInstance('Asset');
 525
 526			// Lock the table for writing.
 527			if (!$asset->_lock())
 528			{
 529				// Error message set in lock method.
 530				return false;
 531			}
 532
 533			if ($asset->loadByName($name))
 534			{
 535				// Delete the node in assets table.
 536				if (!$asset->delete(null, $children))
 537				{
 538					$this->setError($asset->getError());
 539					$asset->_unlock();
 540					return false;
 541				}
 542				$asset->_unlock();
 543			}
 544			else
 545			{
 546				$this->setError($asset->getError());
 547				$asset->_unlock();
 548				return false;
 549			}
 550		}
 551
 552		// Get the node by id.
 553		$node = $this->_getNode($pk);
 554		if (empty($node))
 555		{
 556			// Error message set in getNode method.
 557			$this->_unlock();
 558			return false;
 559		}
 560
 561		// Should we delete all children along with the node?
 562		if ($children)
 563		{
 564			// Delete the node and all of its children.
 565			$query = $this->_db->getQuery(true);
 566			$query->delete()
 567				->from($this->_tbl)
 568				->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
 569			$this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED');
 570
 571			// Compress the left values.
 572			$query = $this->_db->getQuery(true);
 573			$query->update($this->_tbl)
 574				->set('lft = lft - ' . (int) $node->width)
 575				->where('lft > ' . (int) $node->rgt);
 576			$this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED');
 577
 578			// Compress the right values.
 579			$query = $this->_db->getQuery(true);
 580			$query->update($this->_tbl)
 581				->set('rgt = rgt - ' . (int) $node->width)
 582				->where('rgt > ' . (int) $node->rgt);
 583			$this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED');
 584		}
 585		// Leave the children and move them up a level.
 586		else
 587		{
 588			// Delete the node.
 589			$query = $this->_db->getQuery(true);
 590			$query->delete()
 591				->from($this->_tbl)
 592				->where('lft = ' . (int) $node->lft);
 593			$this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED');
 594
 595			// Shift all node's children up a level.
 596			$query->clear()
 597				->update($this->_tbl)
 598				->set('lft = lft - 1')
 599				->set('rgt = rgt - 1')
 600				->set('level = level - 1')
 601				->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
 602			$this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED');
 603
 604			// Adjust all the parent values for direct children of the deleted node.
 605			$query->clear()
 606				->update($this->_tbl)
 607				->set('parent_id = ' . (int) $node->parent_id)
 608				->where('parent_id = ' . (int) $node->$k);
 609			$this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED');
 610
 611			// Shift all of the left values that are right of the node.
 612			$query->clear()
 613				->update($this->_tbl)
 614				->set('lft = lft - 2')
 615				->where('lft > ' . (int) $node->rgt);
 616			$this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED');
 617
 618			// Shift all of the right values that are right of the node.
 619			$query->clear()
 620				->update($this->_tbl)
 621				->set('rgt = rgt - 2')
 622				->where('rgt > ' . (int) $node->rgt);
 623			$this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED');
 624		}
 625
 626		// Unlock the table for writing.
 627		$this->_unlock();
 628
 629		return true;
 630	}
 631
 632	/**
 633	 * Checks that the object is valid and able to be stored.
 634	 *
 635	 * This method checks that the parent_id is non-zero and exists in the database.
 636	 * Note that the root node (parent_id = 0) cannot be manipulated with this class.
 637	 *
 638	 * @return  boolean  True if all checks pass.
 639	 *
 640	 * @since   11.1
 641	 * @throws  RuntimeException on database error.
 642	 */
 643	public function check()
 644	{
 645		$this->parent_id = (int) $this->parent_id;
 646
 647		// Set up a mini exception handler.
 648		try
 649		{
 650			// Check that the parent_id field is valid.
 651			if ($this->parent_id == 0)
 652			{
 653				throw new UnexpectedValueException(sprintf('Invalid `parent_id` [%d] in %s', $this->parent_id, get_class($this)));
 654			}
 655
 656			$query = $this->_db->getQuery(true);
 657			$query->select('COUNT(' . $this->_tbl_key . ')')
 658				->from($this->_tbl)
 659				->where($this->_tbl_key . ' = ' . $this->parent_id);
 660
 661			if (!$this->_db->setQuery($query)->loadResult())
 662			{
 663				throw new UnexpectedValueException(sprintf('Invalid `parent_id` [%d] in %s', $this->parent_id, get_class($this)));
 664			}
 665		}
 666		catch (UnexpectedValueException $e)
 667		{
 668			// Validation error - record it and return false.
 669			$this->setError($e);
 670
 671			return false;
 672		}
 673		// @codeCoverageIgnoreStart
 674		catch (Exception $e)
 675		{
 676			// Database error - rethrow.
 677			throw $e;
 678		}
 679		// @codeCoverageIgnoreEnd
 680
 681		return true;
 682	}
 683
 684	/**
 685	 * Method to store a node in the database table.
 686	 *
 687	 * @param   boolean  $updateNulls  True to update null values as well.
 688	 *
 689	 * @return  boolean  True on success.
 690	 *
 691	 * @link    http://docs.joomla.org/JTableNested/store
 692	 * @since   11.1
 693	 */
 694	public function store($updateNulls = false)
 695	{
 696		$k = $this->_tbl_key;
 697
 698		// @codeCoverageIgnoreStart
 699		if ($this->_debug)
 700		{
 701			echo "\n" . get_class($this) . "::store\n";
 702			$this->_logtable(true, false);
 703		}
 704		// @codeCoverageIgnoreEnd
 705
 706		/*
 707		 * If the primary key is empty, then we assume we are inserting a new node into the
 708		 * tree.  From this point we would need to determine where in the tree to insert it.
 709		 */
 710		if (empty($this->$k))
 711		{
 712			/*
 713			 * We are inserting a node somewhere in the tree with a known reference
 714			 * node.  We have to make room for the new node and set the left and right
 715			 * values before we insert the row.
 716			 */
 717			if ($this->_location_id >= 0)
 718			{
 719				// Lock the table for writing.
 720				if (!$this->_lock())
 721				{
 722					// Error message set in lock method.
 723					return false;
 724				}
 725
 726				// We are inserting a node relative to the last root node.
 727				if ($this->_location_id == 0)
 728				{
 729					// Get the last root node as the reference node.
 730					$query = $this->_db->getQuery(true);
 731					$query->select($this->_tbl_key . ', parent_id, level, lft, rgt')
 732						->from($this->_tbl)
 733						->where('parent_id = 0')
 734						->order('lft DESC');
 735					$this->_db->setQuery($query, 0, 1);
 736					$reference = $this->_db->loadObject();
 737
 738					// @codeCoverageIgnoreStart
 739					if ($this->_debug)
 740					{
 741						$this->_logtable(false);
 742					}
 743					// @codeCoverageIgnoreEnd
 744				}
 745				// We have a real node set as a location reference.
 746				else
 747				{
 748					// Get the reference node by primary key.
 749					if (!$reference = $this->_getNode($this->_location_id))
 750					{
 751						// Error message set in getNode method.
 752						$this->_unlock();
 753						return false;
 754					}
 755				}
 756
 757				// Get the reposition data for shifting the tree and re-inserting the node.
 758				if (!($repositionData = $this->_getTreeRepositionData($reference, 2, $this->_location)))
 759				{
 760					// Error message set in getNode method.
 761					$this->_unlock();
 762					return false;
 763				}
 764
 765				// Create space in the tree at the new location for the new node in left ids.
 766				$query = $this->_db->getQuery(true);
 767				$query->update($this->_tbl)
 768					->set('lft = lft + 2')
 769					->where($repositionData->left_where);
 770				$this->_runQuery($query, 'JLIB_DATABASE_ERROR_STORE_FAILED');
 771
 772				// Create space in the tree at the new location for the new node in right ids.
 773				$query = $this->_db->getQuery(true);
 774				$query->update($this->_tbl)
 775					->set('rgt = rgt + 2')
 776					->where($repositionData->right_where);
 777				$this->_runQuery($query, 'JLIB_DATABASE_ERROR_STORE_FAILED');
 778
 779				// Set the object values.
 780				$this->parent_id = $repositionData->new_parent_id;
 781				$this->level = $repositionData->new_level;
 782				$this->lft = $repositionData->new_lft;
 783				$this->rgt = $repositionData->new_rgt;
 784			}
 785			else
 786			{
 787				// Negative parent ids are invalid
 788				$e = new UnexpectedValueException(sprintf('%s::store() used a negative _location_id', get_class($this)));
 789				$this->setError($e);
 790				return false;
 791			}
 792		}
 793		/*
 794		 * If we have a given primary key then we assume we are simply updating this
 795		 * node in the tree.  We should assess whether or not we are moving the node
 796		 * or just updating its data fields.
 797		 */
 798		else
 799		{
 800			// If the location has been set, move the node to its new location.
 801			if ($this->_location_id > 0)
 802			{
 803				if (!$this->moveByReference($this->_location_id, $this->_location, $this->$k))
 804				{
 805					// Error message set in move method.
 806					return false;
 807				}
 808			}
 809
 810			// Lock the table for writing.
 811			if (!$this->_lock())
 812			{
 813				// Error message set in lock method.
 814				return false;
 815			}
 816		}
 817
 818		// Store the row to the database.
 819		if (!parent::store($updateNulls))
 820		{
 821			$this->_unlock();
 822			return false;
 823		}
 824
 825		// @codeCoverageIgnoreStart
 826		if ($this->_debug)
 827		{
 828			$this->_logtable();
 829		}
 830		// @codeCoverageIgnoreEnd
 831
 832		// Unlock the table for writing.
 833		$this->_unlock();
 834
 835		return true;
 836	}
 837
 838	/**
 839	 * Method to set the publishing state for a node or list of nodes in the database
 840	 * table.  The method respects rows checked out by other users and will attempt
 841	 * to checkin rows that it can after adjustments are made. The method will not
 842	 * allow you to set a publishing state higher than any ancestor node and will
 843	 * not allow you to set a publishing state on a node with a checked out child.
 844	 *
 845	 * @param   mixed    $pks     An optional array of primary key values to update.  If not
 846	 *                            set the instance property value is used.
 847	 * @param   integer  $state   The publishing state. eg. [0 = unpublished, 1 = published]
 848	 * @param   integer  $userId  The user id of the user performing the operation.
 849	 *
 850	 * @return  boolean  True on success.
 851	 *
 852	 * @link    http://docs.joomla.org/JTableNested/publish
 853	 * @since   11.1
 854	 */
 855	public function publish($pks = null, $state = 1, $userId = 0)
 856	{
 857		$k = $this->_tbl_key;
 858
 859		// Sanitize input.
 860		JArrayHelper::toInteger($pks);
 861		$userId = (int) $userId;
 862		$state = (int) $state;
 863
 864		// If $state > 1, then we allow state changes even if an ancestor has lower state
 865		// (for example, can change a child state to Archived (2) if an ancestor is Published (1)
 866		$compareState = ($state > 1) ? 1 : $state;
 867
 868		// If there are no primary keys set check to see if the instance key is set.
 869		if (empty($pks))
 870		{
 871			if ($this->$k)
 872			{
 873				$pks = explode(',', $this->$k);
 874			}
 875			// Nothing to set publishing state on, return false.
 876			else
 877			{
 878				$e = new UnexpectedValueException(sprintf('%s::publish(%s, %d, %d) empty.', get_class($this), $pks, $state, $userId));
 879				$this->setError($e);
 880				return false;
 881			}
 882		}
 883
 884		// Determine if there is checkout support for the table.
 885		$checkoutSupport = (property_exists($this, 'checked_out') || property_exists($this, 'checked_out_time'));
 886
 887		// Iterate over the primary keys to execute the publish action if possible.
 888		foreach ($pks as $pk)
 889		{
 890			// Get the node by primary key.
 891			if (!$node = $this->_getNode($pk))
 892			{
 893				// Error message set in getNode method.
 894				return false;
 895			}
 896
 897			// If the table has checkout support, verify no children are checked out.
 898			if ($checkoutSupport)
 899			{
 900				// Ensure that children are not checked out.
 901				$query = $this->_db->getQuery(true);
 902				$query->select('COUNT(' . $k . ')');
 903				$query->from($this->_tbl);
 904				$query->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
 905				$query->where('(checked_out <> 0 AND checked_out <> ' . (int) $userId . ')');
 906				$this->_db->setQuery($query);
 907
 908				// Check for checked out children.
 909				if ($this->_db->loadResult())
 910				{
 911					// TODO Convert to a conflict exception when available.
 912					$e = new RuntimeException(sprintf('%s::publish(%s, %d, %d) checked-out conflict.', get_class($this), $pks, $state, $userId));
 913					$this->setError($e);
 914					return false;
 915				}
 916			}
 917
 918			// If any parent nodes have lower published state values, we cannot continue.
 919			if ($node->parent_id)
 920			{
 921				// Get any ancestor nodes that have a lower publishing state.
 922				$query = $this->_db->getQuery(true)->select('n.' . $k)->from($this->_db->quoteName($this->_tbl) . ' AS n')
 923					->where('n.lft < ' . (int) $node->lft)->where('n.rgt > ' . (int) $node->rgt)->where('n.parent_id > 0')
 924					->where('n.published < ' . (int) $compareState);
 925
 926				// Just fetch one row (one is one too many).
 927				$this->_db->setQuery($query, 0, 1);
 928
 929				$rows = $this->_db->loadColumn();
 930
 931				if (!empty($rows))
 932				{
 933					$e = new UnexpectedValueException(
 934						sprintf('%s::publish(%s, %d, %d) ancestors have lower state.', get_class($this), $pks, $state, $userId)
 935					);
 936					$this->setError($e);
 937					return false;
 938				}
 939			}
 940
 941			// Update and cascade the publishing state.
 942			$query = $this->_db->getQuery(true)->update($this->_db->quoteName($this->_tbl))->set('published = ' . (int) $state)
 943				->where('(lft > ' . (int) $node->lft . ' AND rgt < ' . (int) $node->rgt . ')' . ' OR ' . $k . ' = ' . (int) $pk);
 944			$this->_db->setQuery($query)->execute();
 945
 946			// If checkout support exists for the object, check the row in.
 947			if ($checkoutSupport)
 948			{
 949				$this->checkin($pk);
 950			}
 951		}
 952
 953		// If the JTable instance value is in the list of primary keys that were set, set the instance.
 954		if (in_array($this->$k, $pks))
 955		{
 956			$this->published = $state;
 957		}
 958
 959		$this->setError('');
 960		return true;
 961	}
 962
 963	/**
 964	 * Method to move a node one position to the left in the same level.
 965	 *
 966	 * @param   integer  $pk  Primary key of the node to move.
 967	 *
 968	 * @return  boolean  True on success.
 969	 *
 970	 * @since   11.1
 971	 * @throws  RuntimeException on database error.
 972	 */
 973	public function orderUp($pk)
 974	{
 975		$k = $this->_tbl_key;
 976		$pk = (is_null($pk)) ? $this->$k : $pk;
 977
 978		// Lock the table for writing.
 979		if (!$this->_lock())
 980		{
 981			// Error message set in lock method.
 982			return false;
 983		}
 984
 985		// Get the node by primary key.
 986		$node = $this->_getNode($pk);
 987		if (empty($node))
 988		{
 989			// Error message set in getNode method.
 990			$this->_unlock();
 991			return false;
 992		}
 993
 994		// Get the left sibling node.
 995		$sibling = $this->_getNode($node->lft - 1, 'right');
 996
 997		if (empty($sibling))
 998		{
 999			// Error message set in getNode method.
1000			$this->_unlock();
1001			return false;
1002		}
1003
1004		try
1005		{
1006			// Get the primary keys of child nodes.
1007			$query = $this->_db->getQuery(true);
1008
1009			$query->select($this->_tbl_key)
1010				->from($this->_tbl)
1011				->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
1012
1013			$children = $this->_db->setQuery($query)->loadColumn();
1014
1015			// Shift left and right values for the node and it's children.
1016			$query->clear()
1017				->update($this->_tbl)
1018				->set('lft = lft - ' . (int) $sibling->width)
1019				->set('rgt = rgt - ' . (int) $sibling->width)
1020				->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
1021			$this->_db->setQuery($query)->execute();
1022
1023			// Shift left and right values for the sibling and it's children.
1024			$query->clear()
1025				->update($this->_tbl)
1026				->set('lft = lft + ' . (int) $node->width)
1027				->set('rgt = rgt + ' . (int) $node->width)
1028				->where('lft BETWEEN ' . (int) $sibling->lft . ' AND ' . (int) $sibling->rgt)
1029				->where($this->_tbl_key . ' NOT IN (' . implode(',', $children) . ')');
1030			$this->_db->setQuery($query)->execute();
1031		}
1032		catch (RuntimeException $e)
1033		{
1034			$this->_unlock();
1035			throw $e;
1036		}
1037
1038		// Unlock the table for writing.
1039		$this->_unlock();
1040
1041		return true;
1042	}
1043
1044	/**
1045	 * Method to move a node one position to the right in the same level.
1046	 *
1047	 * @param   integer  $pk  Primary key of the node to move.
1048	 *
1049	 * @return  boolean  True on success.
1050	 *
1051	 * @since   11.1
1052	 * @throws  RuntimeException on database error.
1053	 */
1054	public function orderDown($pk)
1055	{
1056		$k = $this->_tbl_key;
1057		$pk = (is_null($pk)) ? $this->$k : $pk;
1058
1059		// Lock the table for writing.
1060		if (!$this->_lock())
1061		{
1062			// Error message set in lock method.
1063			return false;
1064		}
1065
1066		// Get the node by primary key.
1067		$node = $this->_getNode($pk);
1068		if (empty($node))
1069		{
1070			// Error message set in getNode method.
1071			$this->_unlock();
1072			return false;
1073		}
1074
1075		$query = $this->_db->getQuery(true);
1076
1077		// Get the right sibling node.
1078		$sibling = $this->_getNode($node->rgt + 1, 'left');
1079		if (empty($sibling))
1080		{
1081			// Error message set in getNode method.
1082			$query->_unlock($this->_db);
1083			$this->_locked = false;
1084			return false;
1085		}
1086
1087		try
1088		{
1089			// Get the primary keys of child nodes.
1090			$query->clear()
1091				->select($this->_tbl_key)
1092				->from($this->_tbl)
1093				->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
1094			$this->_db->setQuery($query);
1095			$children = $this->_db->loadColumn();
1096
1097			// Shift left and right values for the node and it's children.
1098			$query->clear()
1099				->update($this->_tbl)
1100				->set('lft = lft + ' . (int) $sibling->width)
1101				->set('rgt = rgt + ' . (int) $sibling->width)
1102				->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
1103			$this->_db->setQuery($query)->execute();
1104
1105			// Shift left and right values for the sibling and it's children.
1106			$query->clear()
1107				->update($this->_tbl)
1108				->set('lft = lft - ' . (int) $node->width)
1109				->set('rgt = rgt - ' . (int) $node->width)
1110				->where('lft BETWEEN ' . (int) $sibling->lft . ' AND ' . (int) $sibling->rgt)
1111				->where($this->_tbl_key . ' NOT IN (' . implode(',', $children) . ')');
1112			$this->_db->setQuery($query)->execute();
1113		}
1114		catch (RuntimeException $e)
1115		{
1116			$this->_unlock();
1117			throw $e;
1118		}
1119
1120		// Unlock the table for writing.
1121		$this->_unlock();
1122
1123		return true;
1124	}
1125
1126	/**
1127	 * Gets the ID of the root item in the tree
1128	 *
1129	 * @return  mixed  The primary id of the root row, or false if not found and the internal error is set.
1130	 *
1131	 * @since   11.1
1132	 */
1133	public function getRootId()
1134	{
1135		// Get the root item.
1136		$k = $this->_tbl_key;
1137
1138		// Test for a unique record with parent_id = 0
1139		$query = $this->_db->getQuery(true);
1140		$query->select($k)
1141			->from($this->_tbl)
1142			->where('parent_id = 0');
1143
1144		$result = $this->_db->setQuery($query)->loadColumn();
1145
1146		if (count($result) == 1)
1147		{
1148			return $result[0];
1149		}
1150
1151		// Test for a unique record with lft = 0
1152		$query = $this->_db->getQuery(true);
1153		$query->select($k)
1154			->from($this->_tbl)
1155			->where('lft = 0');
1156
1157		$result = $this->_db->setQuery($query)->loadColumn();
1158
1159		if (count($result) == 1)
1160		{
1161			return $result[0];
1162		}
1163
1164		if (property_exists($this, 'alias'))
1165		{
1166			// Test for a unique record alias = root
1167			$query = $this->_db->getQuery(true);
1168			$query->select($k)
1169				->from($this->_tbl)
1170				->where('alias = ' . $this->_db->quote('root'));
1171
1172			$result = $this->_db->setQuery($query)->loadColumn();
1173
1174			if (count($result) == 1)
1175			{
1176				return $result[0];
1177			}
1178		}
1179
1180		$e = new UnexpectedValueException(sprintf('%s::getRootId', get_class($this)));
1181		$this->setError($e);
1182
1183		return false;
1184	}
1185
1186	/**
1187	 * Method to recursively rebuild the whole nested set tree.
1188	 *
1189	 * @param   integer  $parentId  The root of the tree to rebuild.
1190	 * @param   integer  $leftId    The left id to start with in building the tree.
1191	 * @param   integer  $level     The level to assign to the current nodes.
1192	 * @param   string   $path      The path to the current nodes.
1193	 *
1194	 * @return  integer  1 + value of root rgt on success, false on failure
1195	 *
1196	 * @link    http://docs.joomla.org/JTableNested/rebuild
1197	 * @since   11.1
1198	 * @throws  RuntimeException on database error.
1199	 */
1200	public function rebuild($parentId = null, $leftId = 0, $level = 0, $path = '')
1201	{
1202		// If no parent is provided, try to find it.
1203		if ($parentId === null)
1204		{
1205			// Get the root item.
1206			$parentId = $this->getRootId();
1207			if ($parentId === false)
1208			{
1209				return false;
1210			}
1211		}
1212
1213		// Build the structure of the recursive query.
1214		if (!isset($this->_cache['rebuild.sql']))
1215		{
1216			$query = $this->_db->getQuery(true);
1217			$query->select($this->_tbl_key . ', alias')
1218				->from($this->_tbl)
1219				->where('parent_id = %d');
1220
1221			// If the table has an ordering field, use that for ordering.
1222			if (property_exists($this, 'ordering'))
1223			{
1224				$query->order('parent_id, ordering, lft');
1225			}
1226			else
1227			{
1228				$query->order('parent_id, lft');
1229			}
1230			$this->_cache['rebuild.sql'] = (string) $query;
1231		}
1232
1233		// Make a shortcut to database object.
1234
1235		// Assemble the query to find all children of this node.
1236		$this->_db->setQuery(sprintf($this->_cache['rebuild.sql'], (int) $parentId));
1237
1238		$children = $this->_db->loadObjectList();
1239
1240		// The right value of this node is the left value + 1
1241		$rightId = $leftId + 1;
1242
1243		// Execute this function recursively over all children
1244		foreach ($children as $node)
1245		{
1246			/*
1247			 * $rightId is the current right value, which is incremented on recursion return.
1248			 * Increment the level for the children.
1249			 * Add this item's alias to the path (but avoid a leading /)
1250			 */
1251			$rightId = $this->rebuild($node->{$this->_tbl_key}, $rightId, $level + 1, $path . (empty($path) ? '' : '/') . $node->alias);
1252
1253			// If there is an update failure, return false to break out of the recursion.
1254			if ($rightId === false)
1255			{
1256				return false;
1257			}
1258		}
1259
1260		// We've got the left value, and now that we've processed
1261		// the children of this node we also know the right value.
1262		$query = $this->_db->getQuery(true);
1263		$query->update($this->_tbl)
1264			->set('lft = ' . (int) $leftId)
1265			->set('rgt = ' . (int) $rightId)
1266			->set('level = ' . (int) $level)
1267			->set('path = ' . $this->_db->quote($path))
1268			->where($this->_tbl_key . ' = ' . (int) $parentId);
1269		$this->_db->setQuery($query)->execute();
1270
1271		// Return the right value of this node + 1.
1272		return $rightId + 1;
1273	}
1274
1275	/**
1276	 * Method to rebuild the node's path field from the alias values of the
1277	 * nodes from the current node to the root node of the tree.
1278	 *
1279	 * @param   integer  $pk  Primary key of the node for which to get the path.
1280	 *
1281	 * @return  boolean  True on success.
1282	 *
1283	 * @link    http://docs.joomla.org/JTableNested/rebuildPath
1284	 * @since   11.1
1285	 */
1286	public function rebuildPath($pk = null)
1287	{
1288		// If there is no alias or path field, just return true.
1289		if (!property_exists($this, 'alias') || !property_exists($this, 'path'))
1290		{
1291			return true;
1292		}
1293
1294		$k = $this->_tbl_key;
1295		$pk = (is_null($pk)) ? $this->$k : $pk;
1296
1297		// Get the aliases for the path from the node to the root node.
1298		$query = $this->_db->getQuery(true);
1299		$query->select('p.alias');
1300		$query->from($this->_tbl . ' AS n, ' . $this->_tbl . ' AS p');
1301		$query->where('n.lft BETWEEN p.lft AND p.rgt');
1302		$query->where('n.' . $this->_tbl_key . ' = ' . (int) $pk);
1303		$query->order('p.lft');
1304		$this->_db->setQuery($query);
1305
1306		$segments = $this->_db->loadColumn();
1307
1308		// Make sure to remove the root path if it exists in the list.
1309		if ($segments[0] == 'root')
1310		{
1311			array_shift($segments);
1312		}
1313
1314		// Build the path.
1315		$path = trim(implode('/', $segments), ' /\\');
1316
1317		// Update the path field for the node.
1318		$query = $this->_db->getQuery(true);
1319		$query->update($this->_tbl);
1320		$query->set('path = ' . $this->_db->quote($path));
1321		$query->where($this->_tbl_key . ' = ' . (int) $pk);
1322
1323		$this->_db->setQuery($query)->execute();
1324
1325		// Update the current record's path to the new one:
1326		$this->path = $path;
1327
1328		return true;
1329	}
1330
1331	/**
1332	 * Method to update order of table rows
1333	 *
1334	 * @param   array  $idArray    id numbers of rows to be reordered.
1335	 * @param   array  $lft_array  lft values of rows to be reordered.
1336	 *
1337	 * @return  integer  1 + value of root rgt on success, false on failure.
1338	 *
1339	 * @since   11.1
1340	 * @throws  RuntimeException on database error.
1341	 */
1342	public function saveorder($idArray = null, $lft_array = null)
1343	{
1344		try
1345		{
1346			$query = $this->_db->getQuery(true);
1347
1348			// Validate arguments
1349			if (is_array($idArray) && is_array($lft_array) && count($idArray) == count($lft_array))
1350			{
1351				for ($i = 0, $count = count($idArray); $i < $count; $i++)
1352				{
1353					// Do an update to change the lft values in the table for each id
1354					$query->clear()
1355						->update($this->_tbl)
1356						->where($this->_tbl_key . ' = ' . (int) $idArray[$i])
1357						->set('lft = ' . (int) $lft_array[$i]);
1358
1359					$this->_db->setQuery($query)->execute();
1360
1361					// @codeCoverageIgnoreStart
1362					if ($this->_debug)
1363					{
1364						$this->_logtable();
1365					}
1366					// @codeCoverageIgnoreEnd
1367				}
1368
1369				return $this->rebuild();
1370			}
1371			else
1372			{
1373				return false;
1374			}
1375		}
1376		catch (Exception $e)
1377		{
1378			$this->_unlock();
1379			throw $e;
1380		}
1381	}
1382
1383	/**
1384	 * Method to get nested set properties for a node in the tree.
1385	 *
1386	 * @param   integer  $id   Value to look up the node by.
1387	 * @param   string   $key  An optional key to look up the node by (parent | left | right).
1388	 *                         If omitted, the primary key of the table is used.
1389	 *
1390	 * @return  mixed    Boolean false on failure or node object on success.
1391	 *
1392	 * @since   11.1
1393	 * @throws  RuntimeException on database error.
1394	 */
1395	protected function _getNode($id, $key = null)
1396	{
1397		// Determine which key to get the node base on.
1398		switch ($key)
1399		{
1400			case 'parent':
1401				$k = 'parent_id';
1402				break;
1403
1404			case 'left':
1405				$k = 'lft';
1406				break;
1407
1408			case 'right':
1409				$k = 'rgt';
1410				break;
1411
1412			default:
1413				$k = $this->_tbl_key;
1414				break;
1415		}
1416
1417		// Get the node data.
1418		$query = $this->_db->getQuery(true);
1419		$query->select($this->_tbl_key . ', parent_id, level, lft, rgt')
1420			->from($this->_tbl)
1421			->where($k . ' = ' . (int) $id);
1422
1423		$row = $this->_db->setQuery($query, 0, 1)->loadObject();
1424
1425		// Check for no $row returned
1426		if (empty($row))
1427		{
1428			$e = new UnexpectedValueException(sprintf('%s::_getNode(%d, %s) failed.', get_class($this), $id, $key));
1429			$this->setError($e);
1430
1431			return false;
1432		}
1433
1434		// Do some simple calculations.
1435		$row->numChildren = (int) ($row->rgt - $row->lft - 1) / 2;
1436		$row->width = (int) $row->rgt - $row->lft + 1;
1437
1438		return $row;
1439	}
1440
1441	/**
1442	 * Method to get various data necessary to make room in the tree at a location
1443	 * for a node and its children.  The returned data object includes conditions
1444	 * for SQL WHERE clauses for updating left and right id values to make room for
1445	 * the node as well as the new left and right ids for the node.
1446	 *
1447	 * @param   object   $referenceNode  A node object with at least a 'lft' and 'rgt' with
1448	 *                                   which to make room in the tree around for a new node.
1449	 * @param   integer  $nodeWidth      The width of the node for which to make room in the tree.
1450	 * @param   string   $position       The position relative to the reference node where the room
1451	 * should be made.
1452	 *
1453	 * @return  mixed    Boolean false on failure or data object on success.
1454	 *
1455	 * @since   11.1
1456	 */
1457	protected function _getTreeRepositionData($referenceNode, $nodeWidth, $position = 'before')
1458	{
1459		// Make sure the reference an object with a left and right id.
1460		if (!is_object($referenceNode) || !(isset($referenceNode->lft) && isset($referenceNode->rgt)))
1461		{
1462			return false;
1463		}
1464
1465		// A valid node cannot have a width less than 2.
1466		if ($nodeWidth < 2)
1467		{
1468			return false;
1469		}
1470
1471		$k = $this->_tbl_key;
1472		$data = new stdClass;
1473
1474		// Run the calculations and build the data object by reference position.
1475		switch ($position)
1476		{
1477			case 'first-child':
1478				$data->left_where = 'lft > ' . $referenceNode->lft;
1479				$data->right_where = 'rgt >= ' . $referenceNode->lft;
1480
1481				$data->new_lft = $referenceNode->lft + 1;
1482				$data->new_rgt = $referenceNode->lft + $nodeWidth;
1483				$data->new_parent_id = $referenceNode->$k;
1484				$data->new_level = $referenceNode->level + 1;
1485				break;
1486
1487			case 'last-child':
1488				$data->left_where = 'lft > ' . ($referenceNode->rgt);
1489				$data->right_where = 'rgt >= ' . ($referenceNode->rgt);
1490
1491				$data->new_lft = $referenceNode->rgt;
1492				$data->new_rgt = $referenceNode->rgt + $nodeWidth - 1;
1493				$data->new_parent_id = $referenceNode->$k;
1494				$data->new_level = $referenceNode->level + 1;
1495				break;
1496
1497			case 'before':
1498				$data->left_where = 'lft >= ' . $referenceNode->lft;
1499				$data->right_where = 'rgt >= ' . $referenceNode->lft;
1500
1501				$data->new_lft = $referenceNode->lft;
1502				$data->new_rgt = $referenceNode->lft + $nodeWidth - 1;
1503				$data->new_parent_id = $referenceNode->parent_id;
1504				$data->new_level = $referenceNode->level;
1505				break;
1506
1507			default:
1508			case 'after':
1509				$data->left_where = 'lft > ' . $referenceNode->rgt;
1510				$data->right_where = 'rgt > ' . $referenceNode->rgt;
1511
1512				$data->new_lft = $referenceNode->rgt + 1;
1513				$data->new_rgt = $referenceNode->rgt + $nodeWidth;
1514				$data->new_parent_id = $referenceNode->parent_id;
1515				$data->new_level = $referenceNode->level;
1516				break;
1517		}
1518
1519		// @codeCoverageIgnoreStart
1520		if ($this->_debug)
1521		{
1522			echo "\nRepositioning Data for $position" . "\n-----------------------------------" . "\nLeft Where:    $data->left_where"
1523				. "\nRight Where:   $data->right_where" . "\nNew Lft:       $data->new_lft" . "\nNew Rgt:       $data->new_rgt"
1524				. "\nNew Parent ID: $data->new_parent_id" . "\nNew Level:     $data->new_level" . "\n";
1525		}
1526		// @codeCoverageIgnoreEnd
1527
1528		return $data;
1529	}
1530
1531	/**
1532	 * Method to create a log table in the buffer optionally showing the query and/or data.
1533	 *
1534	 * @param   boolean  $showData   True to show data
1535	 * @param   boolean  $showQuery  True to show query
1536	 *
1537	 * @return  void
1538	 *
1539	 * @codeCoverageIgnore
1540	 * @since   11.1
1541	 */
1542	protected function _logtable($showData = true, $showQuery = true)
1543	{
1544		$sep = "\n" . str_pad('', 40, '-');
1545		$buffer = '';
1546		if ($showQuery)
1547		{
1548			$buffer .= "\n" . $this->_db->getQuery() . $sep;
1549		}
1550
1551		if ($showData)
1552		{
1553			$query = $this->_db->getQuery(true);
1554			$query->select($this->_tbl_key . ', parent_id, lft, rgt, level');
1555			$query->from($this->_tbl);
1556			$query->order($this->_tbl_key);
1557			$this->_db->setQuery($query);
1558
1559			$rows = $this->_db->loadRowList();
1560			$buffer .= sprintf("\n| %4s | %4s | %4s | %4s |", $this->_tbl_key, 'par', 'lft', 'rgt');
1561			$buffer .= $sep;
1562
1563			foreach ($rows as $row)
1564			{
1565				$buffer .= sprintf("\n| %4s | %4s | %4s | %4s |", $row[0], $row[1], $row[2], $row[3]);
1566			}
1567			$buffer .= $sep;
1568		}
1569		echo $buffer;
1570	}
1571
1572	/**
1573	 * Runs a query and unlocks the database on an error.
1574	 *
1575	 * @param   mixed   $query         A string or JDatabaseQuery object.
1576	 * @param   string  $errorMessage  Unused.
1577	 *
1578	 * @return  boolean  void
1579	 *
1580	 * @note    Since 12.1 this method returns void and will rethrow the database exception.
1581	 * @since   11.1
1582	 * @throws  RuntimeException on database error.
1583	 */
1584	protected function _runQuery($query, $errorMessage)
1585	{
1586		// Prepare to catch an exception.
1587		try
1588		{
1589			$this->_db->setQuery($query)->execute();
1590
1591			// @codeCoverageIgnoreStart
1592			if ($this->_debug)
1593			{
1594				$this->_logtable();
1595			}
1596			// @codeCoverageIgnoreEnd
1597		}
1598		catch (Exception $e)
1599		{
1600			// Unlock the tables and rethrow.
1601			$this->_unlock();
1602
1603			throw $e;
1604		}
1605	}
1606}