<?php
/**
 * Joomla! Content Management System
 *
 * @copyright  (C) 2005 Open Source Matters, Inc. <https://www.joomla.org>
 * @license    GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace Joomla\CMS\Table;

\defined('JPATH_PLATFORM') or die;

use Joomla\CMS\Access\Rules;
use Joomla\CMS\Event\AbstractEvent;
use Joomla\CMS\Factory;
use Joomla\CMS\Filesystem\Path;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Object\CMSObject;
use Joomla\Database\DatabaseDriver;
use Joomla\Database\DatabaseQuery;
use Joomla\Event\DispatcherAwareInterface;
use Joomla\Event\DispatcherAwareTrait;
use Joomla\Event\DispatcherInterface;
use Joomla\String\StringHelper;

/**
 * Abstract Table class
 *
 * Parent class to all tables.
 *
 * @since  1.7.0
 * @tutorial  Joomla.Platform/jtable.cls
 */
abstract class Table extends CMSObject implements TableInterface, DispatcherAwareInterface
{
	use DispatcherAwareTrait;

	/**
	 * Include paths for searching for Table classes.
	 *
	 * @var    array
	 * @since  3.0.0
	 */
	private static $_includePaths = array();

	/**
	 * Name of the database table to model.
	 *
	 * @var    string
	 * @since  1.7.0
	 */
	protected $_tbl = '';

	/**
	 * Name of the primary key field in the table.
	 *
	 * @var    string
	 * @since  1.7.0
	 */
	protected $_tbl_key = '';

	/**
	 * Name of the primary key fields in the table.
	 *
	 * @var    array
	 * @since  3.0.1
	 */
	protected $_tbl_keys = array();

	/**
	 * DatabaseDriver object.
	 *
	 * @var    DatabaseDriver
	 * @since  1.7.0
	 */
	protected $_db;

	/**
	 * Should rows be tracked as ACL assets?
	 *
	 * @var    boolean
	 * @since  1.7.0
	 */
	protected $_trackAssets = false;

	/**
	 * The rules associated with this record.
	 *
	 * @var    Rules  A Rules object.
	 * @since  1.7.0
	 */
	protected $_rules;

	/**
	 * Indicator that the tables have been locked.
	 *
	 * @var    boolean
	 * @since  1.7.0
	 */
	protected $_locked = false;

	/**
	 * Indicates that the primary keys autoincrement.
	 *
	 * @var    boolean
	 * @since  3.1.4
	 */
	protected $_autoincrement = true;

	/**
	 * Array with alias for "special" columns such as ordering, hits etc etc
	 *
	 * @var    array
	 * @since  3.4.0
	 */
	protected $_columnAlias = array();

	/**
	 * An array of key names to be json encoded in the bind function
	 *
	 * @var    array
	 * @since  3.3
	 */
	protected $_jsonEncode = array();

	/**
	 * Indicates that columns fully support the NULL value in the database
	 *
	 * @var    boolean
	 * @since  3.10.0
	 */
	protected $_supportNullValue = false;

	/**
	 * The UCM type alias. Used for tags, content versioning etc. Leave blank to effectively disable these features.
	 *
	 * @var    string
	 * @since  4.0.0
	 */
	public $typeAlias = null;

	/**
	 * Object constructor to set table and key fields.  In most cases this will
	 * be overridden by child classes to explicitly set the table and key fields
	 * for a particular database table.
	 *
	 * @param   string               $table       Name of the table to model.
	 * @param   mixed                $key         Name of the primary key field in the table or array of field names that compose the primary key.
	 * @param   DatabaseDriver       $db          DatabaseDriver object.
	 * @param   DispatcherInterface  $dispatcher  Event dispatcher for this table
	 *
	 * @since   1.7.0
	 */
	public function __construct($table, $key, DatabaseDriver $db, DispatcherInterface $dispatcher = null)
	{
		parent::__construct();

		// Set internal variables.
		$this->_tbl = $table;

		// Set the key to be an array.
		if (\is_string($key))
		{
			$key = array($key);
		}
		elseif (\is_object($key))
		{
			$key = (array) $key;
		}

		$this->_tbl_keys = $key;

		if (\count($key) == 1)
		{
			$this->_autoincrement = true;
		}
		else
		{
			$this->_autoincrement = false;
		}

		// Set the singular table key for backwards compatibility.
		$this->_tbl_key = $this->getKeyName();

		$this->_db = $db;

		// Initialise the table properties.
		$fields = $this->getFields();

		if ($fields)
		{
			foreach ($fields as $name => $v)
			{
				// Add the field if it is not already present.
				if (!$this->hasField($name))
				{
					$this->$name = null;
				}
			}
		}

		// If we are tracking assets, make sure an access field exists and initially set the default.
		if ($this->hasField('asset_id'))
		{
			$this->_trackAssets = true;
		}

		// If the access property exists, set the default.
		if ($this->hasField('access'))
		{
			$this->access = (int) Factory::getApplication()->get('access');
		}

		// Create or set a Dispatcher
		if (!\is_object($dispatcher) || !($dispatcher instanceof DispatcherInterface))
		{
			// TODO Maybe we should use a dedicated "behaviour" dispatcher for performance reasons and to prevent system plugins from butting in?
			$dispatcher = Factory::getApplication()->getDispatcher();
		}

		$this->setDispatcher($dispatcher);

		$event = AbstractEvent::create(
			'onTableObjectCreate',
			[
				'subject'	=> $this,
			]
		);
		$this->getDispatcher()->dispatch('onTableObjectCreate', $event);
	}

	/**
	 * Get the columns from database table.
	 *
	 * @param   bool  $reload  flag to reload cache
	 *
	 * @return  mixed  An array of the field names, or false if an error occurs.
	 *
	 * @since   1.7.0
	 * @throws  \UnexpectedValueException
	 */
	public function getFields($reload = false)
	{
		static $cache = null;

		if ($cache === null || $reload)
		{
			// Lookup the fields for this table only once.
			$name   = $this->_tbl;
			$fields = $this->_db->getTableColumns($name, false);

			if (empty($fields))
			{
				throw new \UnexpectedValueException(sprintf('No columns found for %s table', $name));
			}

			$cache = $fields;
		}

		return $cache;
	}

	/**
	 * Static method to get an instance of a Table class if it can be found in the table include paths.
	 *
	 * To add include paths for searching for Table classes see Table::addIncludePath().
	 *
	 * @param   string  $type    The type (name) of the Table class to get an instance of.
	 * @param   string  $prefix  An optional prefix for the table class name.
	 * @param   array   $config  An optional array of configuration values for the Table object.
	 *
	 * @return  Table|boolean   A Table object if found or boolean false on failure.
	 *
	 * @since       1.7.0
	 * @deprecated  5.0 Use the MvcFactory instead
	 */
	public static function getInstance($type, $prefix = 'JTable', $config = array())
	{
		// Sanitize and prepare the table class name.
		$type       = preg_replace('/[^A-Z0-9_\.-]/i', '', $type);
		$tableClass = $prefix . ucfirst($type);

		// Only try to load the class if it doesn't already exist.
		if (!class_exists($tableClass))
		{
			// Search for the class file in the JTable include paths.
			$paths = self::addIncludePath();
			$pathIndex = 0;

			while (!class_exists($tableClass) && $pathIndex < \count($paths))
			{
				if ($tryThis = Path::find($paths[$pathIndex++], strtolower($type) . '.php'))
				{
					// Import the class file.
					include_once $tryThis;
				}
			}

			if (!class_exists($tableClass))
			{
				/*
				* If unable to find the class file in the Table include paths. Return false.
				* The warning JLIB_DATABASE_ERROR_NOT_SUPPORTED_FILE_NOT_FOUND has been removed in 3.6.3.
				* In 4.0 an Exception (type to be determined) will be thrown.
				* For more info see https://github.com/joomla/joomla-cms/issues/11570
				*/

				return false;
			}
		}

		// If a database object was passed in the configuration array use it, otherwise get the global one from Factory.
		$db = $config['dbo'] ?? Factory::getDbo();

		// Check for a possible service from the container otherwise manually instantiate the class
		if (Factory::getContainer()->has($tableClass))
		{
			return Factory::getContainer()->get($tableClass);
		}

		// Instantiate a new table class and return it.
		return new $tableClass($db);
	}

	/**
	 * Add a filesystem path where Table should search for table class files.
	 *
	 * @param   array|string  $path  A filesystem path or array of filesystem paths to add.
	 *
	 * @return  array  An array of filesystem paths to find Table classes in.
	 *
	 * @since       1.7.0
	 * @deprecated  5.0 Should not be used anymore as tables are loaded through the MvcFactory
	 */
	public static function addIncludePath($path = null)
	{
		// If the internal paths have not been initialised, do so with the base table path.
		if (empty(self::$_includePaths))
		{
			self::$_includePaths = array(__DIR__);
		}

		// Convert the passed path(s) to add to an array.
		settype($path, 'array');

		// If we have new paths to add, do so.
		if (!empty($path))
		{
			// Check and add each individual new path.
			foreach ($path as $dir)
			{
				// Sanitize path.
				$dir = trim($dir);

				// Add to the front of the list so that custom paths are searched first.
				if (!\in_array($dir, self::$_includePaths))
				{
					array_unshift(self::$_includePaths, $dir);
				}
			}
		}

		return self::$_includePaths;
	}

	/**
	 * Method to compute the default name of the asset.
	 * The default name is in the form table_name.id
	 * where id is the value of the primary key of the table.
	 *
	 * @return  string
	 *
	 * @since   1.7.0
	 */
	protected function _getAssetName()
	{
		$keys = array();

		foreach ($this->_tbl_keys as $k)
		{
			$keys[] = (int) $this->$k;
		}

		return $this->_tbl . '.' . implode('.', $keys);
	}

	/**
	 * Method to return the title to use for the asset table.
	 *
	 * In tracking the assets a title is kept for each asset so that there is some context available in a unified access manager.
	 * Usually this would just return $this->title or $this->name or whatever is being used for the primary name of the row.
	 * If this method is not overridden, the asset name is used.
	 *
	 * @return  string  The string to use as the title in the asset table.
	 *
	 * @since   1.7.0
	 */
	protected function _getAssetTitle()
	{
		return $this->_getAssetName();
	}

	/**
	 * Method to get the parent asset under which to register this one.
	 *
	 * By default, all assets are registered to the ROOT node with ID, which will default to 1 if none exists.
	 * An extended class can define a table and ID to lookup.  If the asset does not exist it will be created.
	 *
	 * @param   Table    $table  A Table object for the asset parent.
	 * @param   integer  $id     Id to look up
	 *
	 * @return  integer
	 *
	 * @since   1.7.0
	 */
	protected function _getAssetParentId(Table $table = null, $id = null)
	{
		// For simple cases, parent to the asset root.
		/** @var Asset $assets */
		$assets = self::getInstance('Asset', 'JTable', array('dbo' => $this->getDbo()));
		$rootId = $assets->getRootId();

		if (!empty($rootId))
		{
			return $rootId;
		}

		return 1;
	}

	/**
	 * Method to append the primary keys for this table to a query.
	 *
	 * @param   DatabaseQuery  $query  A query object to append.
	 * @param   mixed          $pk     Optional primary key parameter.
	 *
	 * @return  void
	 *
	 * @since   3.1.4
	 */
	public function appendPrimaryKeys($query, $pk = null)
	{
		if (\is_null($pk))
		{
			foreach ($this->_tbl_keys as $k)
			{
				$query->where($this->_db->quoteName($k) . ' = ' . $this->_db->quote($this->$k));
			}
		}
		else
		{
			if (\is_string($pk))
			{
				$pk = array($this->_tbl_key => $pk);
			}

			$pk = (object) $pk;

			foreach ($this->_tbl_keys as $k)
			{
				$query->where($this->_db->quoteName($k) . ' = ' . $this->_db->quote($pk->$k));
			}
		}
	}

	/**
	 * Method to get the database table name for the class.
	 *
	 * @return  string  The name of the database table being modeled.
	 *
	 * @since   1.7.0
	 */
	public function getTableName()
	{
		return $this->_tbl;
	}

	/**
	 * Method to get the primary key field name for the table.
	 *
	 * @param   boolean  $multiple  True to return all primary keys (as an array) or false to return just the first one (as a string).
	 *
	 * @return  mixed  Array of primary key field names or string containing the first primary key field.
	 *
	 * @since   1.7.0
	 */
	public function getKeyName($multiple = false)
	{
		// Count the number of keys
		if (\count($this->_tbl_keys))
		{
			if ($multiple)
			{
				// If we want multiple keys, return the raw array.
				return $this->_tbl_keys;
			}
			else
			{
				// If we want the standard method, just return the first key.
				return $this->_tbl_keys[0];
			}
		}

		return '';
	}

	/**
	 * Returns the identity (primary key) value of this record
	 *
	 * @return  mixed
	 *
	 * @since   4.0.0
	 */
	public function getId()
	{
		$key = $this->getKeyName();

		return $this->$key;
	}

	/**
	 * Method to get the DatabaseDriver object.
	 *
	 * @return  DatabaseDriver  The internal database driver object.
	 *
	 * @since   1.7.0
	 */
	public function getDbo()
	{
		return $this->_db;
	}

	/**
	 * Method to set the DatabaseDriver object.
	 *
	 * @param   DatabaseDriver  $db  A DatabaseDriver object to be used by the table object.
	 *
	 * @return  boolean  True on success.
	 *
	 * @since   1.7.0
	 */
	public function setDbo(DatabaseDriver $db)
	{
		$this->_db = $db;

		return true;
	}

	/**
	 * Method to set rules for the record.
	 *
	 * @param   mixed  $input  A Rules object, JSON string, or array.
	 *
	 * @return  void
	 *
	 * @since   1.7.0
	 */
	public function setRules($input)
	{
		if ($input instanceof Rules)
		{
			$this->_rules = $input;
		}
		else
		{
			$this->_rules = new Rules($input);
		}
	}

	/**
	 * Method to get the rules for the record.
	 *
	 * @return  Rules object
	 *
	 * @since   1.7.0
	 */
	public function getRules()
	{
		return $this->_rules;
	}

	/**
	 * Method to reset class properties to the defaults set in the class
	 * definition. It will ignore the primary key as well as any private class
	 * properties (except $_errors).
	 *
	 * @return  void
	 *
	 * @since   1.7.0
	 */
	public function reset()
	{
		$event = AbstractEvent::create(
			'onTableBeforeReset',
			[
				'subject'	=> $this,
			]
		);
		$this->getDispatcher()->dispatch('onTableBeforeReset', $event);

		// Get the default values for the class from the table.
		foreach ($this->getFields() as $k => $v)
		{
			// If the property is not the primary key or private, reset it.
			if (!\in_array($k, $this->_tbl_keys) && (strpos($k, '_') !== 0))
			{
				$this->$k = $v->Default;
			}
		}

		// Reset table errors
		$this->_errors = array();

		$event = AbstractEvent::create(
			'onTableAfterReset',
			[
				'subject'	=> $this,
			]
		);
		$this->getDispatcher()->dispatch('onTableAfterReset', $event);
	}

	/**
	 * Method to bind an associative array or object to the Table instance.This
	 * method only binds properties that are publicly accessible and optionally
	 * takes an array of properties to ignore when binding.
	 *
	 * @param   array|object  $src     An associative array or object to bind to the Table instance.
	 * @param   array|string  $ignore  An optional array or space separated list of properties to ignore while binding.
	 *
	 * @return  boolean  True on success.
	 *
	 * @since   1.7.0
	 * @throws  \InvalidArgumentException
	 */
	public function bind($src, $ignore = array())
	{
		// Check if the source value is an array or object
		if (!\is_object($src) && !\is_array($src))
		{
			throw new \InvalidArgumentException(
				sprintf(
					'Could not bind the data source in %1$s::bind(), the source must be an array or object but a "%2$s" was given.',
					\get_class($this),
					\gettype($src)
				)
			);
		}

		// If the ignore value is a string, explode it over spaces.
		if (!\is_array($ignore))
		{
			$ignore = explode(' ', $ignore);
		}

		$event = AbstractEvent::create(
			'onTableBeforeBind',
			[
				'subject'	=> $this,
				'src'		=> $src,
				'ignore'	=> $ignore
			]
		);
		$this->getDispatcher()->dispatch('onTableBeforeBind', $event);

		// If the source value is an object, get its accessible properties.
		if (\is_object($src))
		{
			$src = get_object_vars($src);
		}

		// JSON encode any fields required
		if (!empty($this->_jsonEncode))
		{
			foreach ($this->_jsonEncode as $field)
			{
				if (isset($src[$field]) && \is_array($src[$field]))
				{
					$src[$field] = json_encode($src[$field]);
				}
			}
		}

		// Bind the source value, excluding the ignored fields.
		foreach ($this->getProperties() as $k => $v)
		{
			// Only process fields not in the ignore array.
			if (!\in_array($k, $ignore))
			{
				if (isset($src[$k]))
				{
					$this->$k = $src[$k];
				}
			}
		}

		$event = AbstractEvent::create(
			'onTableAfterBind',
			[
				'subject'	=> $this,
				'src'		=> $src,
				'ignore'	=> $ignore
			]
		);
		$this->getDispatcher()->dispatch('onTableAfterBind', $event);

		return true;
	}

	/**
	 * Method to load a row from the database by primary key and bind the fields to the Table instance properties.
	 *
	 * @param   mixed    $keys   An optional primary key value to load the row by, or an array of fields to match.
	 *                           If not set the instance property value is used.
	 * @param   boolean  $reset  True to reset the default values before loading the new row.
	 *
	 * @return  boolean  True if successful. False if row not found.
	 *
	 * @since   1.7.0
	 * @throws  \InvalidArgumentException
	 * @throws  \RuntimeException
	 * @throws  \UnexpectedValueException
	 */
	public function load($keys = null, $reset = true)
	{
		// Pre-processing by observers
		$event = AbstractEvent::create(
			'onTableBeforeLoad',
			[
				'subject'	=> $this,
				'keys'		=> $keys,
				'reset'		=> $reset,
			]
		);
		$this->getDispatcher()->dispatch('onTableBeforeLoad', $event);

		if (empty($keys))
		{
			$empty = true;
			$keys  = array();

			// If empty, use the value of the current key
			foreach ($this->_tbl_keys as $key)
			{
				$empty      = $empty && empty($this->$key);
				$keys[$key] = $this->$key;
			}

			// If empty primary key there's is no need to load anything
			if ($empty)
			{
				return true;
			}
		}
		elseif (!\is_array($keys))
		{
			// Load by primary key.
			$keyCount = \count($this->_tbl_keys);

			if ($keyCount)
			{
				if ($keyCount > 1)
				{
					throw new \InvalidArgumentException('Table has multiple primary keys specified, only one primary key value provided.');
				}

				$keys = array($this->getKeyName() => $keys);
			}
			else
			{
				throw new \RuntimeException('No table keys defined.');
			}
		}

		if ($reset)
		{
			$this->reset();
		}

		// Initialise the query.
		$query = $this->_db->getQuery(true)
			->select('*')
			->from($this->_tbl);
		$fields = array_keys($this->getProperties());

		foreach ($keys as $field => $value)
		{
			// Check that $field is in the table.
			if (!\in_array($field, $fields))
			{
				throw new \UnexpectedValueException(sprintf('Missing field in database: %s &#160; %s.', \get_class($this), $field));
			}

			// Add the search tuple to the query.
			$query->where($this->_db->quoteName($field) . ' = ' . $this->_db->quote($value));
		}

		$this->_db->setQuery($query);

		$row = $this->_db->loadAssoc();

		// Check that we have a result.
		if (empty($row))
		{
			$result = false;
		}
		else
		{
			// Bind the object with the row and return.
			$result = $this->bind($row);
		}

		// Post-processing by observers
		$event = AbstractEvent::create(
			'onTableAfterLoad',
			[
				'subject'		=> $this,
				'result'		=> &$result,
				'row'			=> $row,
			]
		);
		$this->getDispatcher()->dispatch('onTableAfterLoad', $event);

		return $result;
	}

	/**
	 * Method to perform sanity checks on the Table instance properties to ensure they are safe to store in the database.
	 *
	 * Child classes should override this method to make sure the data they are storing in the database is safe and as expected before storage.
	 *
	 * @return  boolean  True if the instance is sane and able to be stored in the database.
	 *
	 * @since   1.7.0
	 */
	public function check()
	{
		// Post-processing by observers
		$event = AbstractEvent::create(
			'onTableCheck',
			[
				'subject'		=> $this,
			]
		);
		$this->getDispatcher()->dispatch('onTableCheck', $event);

		return true;
	}

	/**
	 * Method to store a row in the database from the Table instance properties.
	 *
	 * If a primary key value is set the row with that primary key value will be updated with the instance property values.
	 * If no primary key value is set a new row will be inserted into the database with the properties from the Table instance.
	 *
	 * @param   boolean  $updateNulls  True to update fields even if they are null.
	 *
	 * @return  boolean  True on success.
	 *
	 * @since   1.7.0
	 */
	public function store($updateNulls = false)
	{
		$result = true;

		$k = $this->_tbl_keys;

		// Pre-processing by observers
		$event = AbstractEvent::create(
			'onTableBeforeStore',
			[
				'subject'		=> $this,
				'updateNulls'	=> $updateNulls,
				'k'				=> $k,
			]
		);
		$this->getDispatcher()->dispatch('onTableBeforeStore', $event);

		$currentAssetId = 0;

		if (!empty($this->asset_id))
		{
			$currentAssetId = $this->asset_id;
		}

		// The asset id field is managed privately by this class.
		if ($this->_trackAssets)
		{
			unset($this->asset_id);
		}

		// We have to unset typeAlias since updateObject / insertObject will try to insert / update all public variables...
		$typeAlias = $this->typeAlias;
		unset($this->typeAlias);

		try
		{
			// If a primary key exists update the object, otherwise insert it.
			if ($this->hasPrimaryKey())
			{
				$this->_db->updateObject($this->_tbl, $this, $this->_tbl_keys, $updateNulls);
			}
			else
			{
				$this->_db->insertObject($this->_tbl, $this, $this->_tbl_keys[0]);
			}
		}
		catch (\Exception $e)
		{
			$this->setError($e->getMessage());
			$result = false;
		}

		$this->typeAlias = $typeAlias;

		// If the table is not set to track assets return true.
		if ($this->_trackAssets)
		{
			if ($this->_locked)
			{
				$this->_unlock();
			}

			/*
			 * Asset Tracking
			 */
			$parentId = $this->_getAssetParentId();
			$name     = $this->_getAssetName();
			$title    = $this->_getAssetTitle();

			/** @var Asset $asset */
			$asset = self::getInstance('Asset', 'JTable', array('dbo' => $this->getDbo()));
			$asset->loadByName($name);

			// Re-inject the asset id.
			$this->asset_id = $asset->id;

			// Check for an error.
			$error = $asset->getError();

			if ($error)
			{
				$this->setError($error);

				return false;
			}
			else
			{
				// Specify how a new or moved node asset is inserted into the tree.
				if (empty($this->asset_id) || $asset->parent_id != $parentId)
				{
					$asset->setLocation($parentId, 'last-child');
				}

				// Prepare the asset to be stored.
				$asset->parent_id = $parentId;
				$asset->name      = $name;

				// Respect the table field limits
				$asset->title = StringHelper::substr($title, 0, 100);

				if ($this->_rules instanceof Rules)
				{
					$asset->rules = (string) $this->_rules;
				}

				if (!$asset->check() || !$asset->store($updateNulls))
				{
					$this->setError($asset->getError());

					return false;
				}
				else
				{
					// Create an asset_id or heal one that is corrupted.
					if (empty($this->asset_id) || ($currentAssetId != $this->asset_id && !empty($this->asset_id)))
					{
						// Update the asset_id field in this table.
						$this->asset_id = (int) $asset->id;

						$query = $this->_db->getQuery(true)
							->update($this->_db->quoteName($this->_tbl))
							->set('asset_id = ' . (int) $this->asset_id);
						$this->appendPrimaryKeys($query);
						$this->_db->setQuery($query)->execute();
					}
				}
			}
		}

		// Post-processing by observers
		$event = AbstractEvent::create(
			'onTableAfterStore',
			[
				'subject'	=> $this,
				'result'	=> &$result,
			]
		);
		$this->getDispatcher()->dispatch('onTableAfterStore', $event);

		return $result;
	}

	/**
	 * Method to provide a shortcut to binding, checking and storing a Table instance to the database table.
	 *
	 * The method will check a row in once the data has been stored and if an ordering filter is present will attempt to reorder
	 * the table rows based on the filter.  The ordering filter is an instance property name.  The rows that will be reordered
	 * are those whose value matches the Table instance for the property specified.
	 *
	 * @param   array|object  $src             An associative array or object to bind to the Table instance.
	 * @param   string        $orderingFilter  Filter for the order updating
	 * @param   array|string  $ignore          An optional array or space separated list of properties to ignore while binding.
	 *
	 * @return  boolean  True on success.
	 *
	 * @since   1.7.0
	 */
	public function save($src, $orderingFilter = '', $ignore = '')
	{
		// Attempt to bind the source to the instance.
		if (!$this->bind($src, $ignore))
		{
			return false;
		}

		// Run any sanity checks on the instance and verify that it is ready for storage.
		if (!$this->check())
		{
			return false;
		}

		// Attempt to store the properties to the database table.
		if (!$this->store())
		{
			return false;
		}

		// Attempt to check the row in, just in case it was checked out.
		if (!$this->checkIn())
		{
			return false;
		}

		// If an ordering filter is set, attempt reorder the rows in the table based on the filter and value.
		if ($orderingFilter)
		{
			$filterValue = $this->$orderingFilter;
			$this->reorder($orderingFilter ? $this->_db->quoteName($orderingFilter) . ' = ' . $this->_db->quote($filterValue) : '');
		}

		// Set the error to empty and return true.
		$this->setError('');

		return true;
	}

	/**
	 * Method to delete a row from the database table by primary key value.
	 *
	 * @param   mixed  $pk  An optional primary key value to delete.  If not set the instance property value is used.
	 *
	 * @return  boolean  True on success.
	 *
	 * @since   1.7.0
	 * @throws  \UnexpectedValueException
	 */
	public function delete($pk = null)
	{
		if (\is_null($pk))
		{
			$pk = array();

			foreach ($this->_tbl_keys as $key)
			{
				$pk[$key] = $this->$key;
			}
		}
		elseif (!\is_array($pk))
		{
			$pk = array($this->_tbl_key => $pk);
		}

		foreach ($this->_tbl_keys as $key)
		{
			$pk[$key] = \is_null($pk[$key]) ? $this->$key : $pk[$key];

			if ($pk[$key] === null)
			{
				throw new \UnexpectedValueException('Null primary key not allowed.');
			}

			$this->$key = $pk[$key];
		}

		// Pre-processing by observers
		$event = AbstractEvent::create(
			'onTableBeforeDelete',
			[
				'subject'	=> $this,
				'pk'		=> $pk,
			]
		);
		$this->getDispatcher()->dispatch('onTableBeforeDelete', $event);

		// If tracking assets, remove the asset first.
		if ($this->_trackAssets)
		{
			// Get the asset name
			$name  = $this->_getAssetName();

			/** @var Asset $asset */
			$asset = self::getInstance('Asset');

			if ($asset->loadByName($name))
			{
				if (!$asset->delete())
				{
					$this->setError($asset->getError());

					return false;
				}
			}
		}

		// Delete the row by primary key.
		$query = $this->_db->getQuery(true)
			->delete($this->_tbl);
		$this->appendPrimaryKeys($query, $pk);

		$this->_db->setQuery($query);

		// Check for a database error.
		$this->_db->execute();

		// Post-processing by observers
		$event = AbstractEvent::create(
			'onTableAfterDelete',
			[
				'subject'	=> $this,
				'pk'		=> $pk,
			]
		);
		$this->getDispatcher()->dispatch('onTableAfterDelete', $event);

		return true;
	}

	/**
	 * Method to check a row out if the necessary properties/fields exist.
	 *
	 * To prevent race conditions while editing rows in a database, a row can be checked out if the fields 'checked_out' and 'checked_out_time'
	 * are available. While a row is checked out, any attempt to store the row by a user other than the one who checked the row out should be
	 * held until the row is checked in again.
	 *
	 * @param   integer  $userId  The Id of the user checking out the row.
	 * @param   mixed    $pk      An optional primary key value to check out.  If not set the instance property value is used.
	 *
	 * @return  boolean  True on success.
	 *
	 * @since   1.7.0
	 * @throws  \UnexpectedValueException
	 */
	public function checkOut($userId, $pk = null)
	{
		// Pre-processing by observers
		$event = AbstractEvent::create(
			'onTableBeforeCheckout',
			[
				'subject'	=> $this,
				'userId'	=> $userId,
				'pk'		=> $pk,
			]
		);
		$this->getDispatcher()->dispatch('onTableBeforeCheckout', $event);

		// If there is no checked_out or checked_out_time field, just return true.
		if (!$this->hasField('checked_out') || !$this->hasField('checked_out_time'))
		{
			return true;
		}

		if (\is_null($pk))
		{
			$pk = array();

			foreach ($this->_tbl_keys as $key)
			{
				$pk[$key] = $this->$key;
			}
		}
		elseif (!\is_array($pk))
		{
			$pk = array($this->_tbl_key => $pk);
		}

		foreach ($this->_tbl_keys as $key)
		{
			$pk[$key] = \is_null($pk[$key]) ? $this->$key : $pk[$key];

			if ($pk[$key] === null)
			{
				throw new \UnexpectedValueException('Null primary key not allowed.');
			}
		}

		// Get column names.
		$checkedOutField     = $this->getColumnAlias('checked_out');
		$checkedOutTimeField = $this->getColumnAlias('checked_out_time');

		// Get the current time in the database format.
		$time = Factory::getDate()->toSql();

		// Check the row out by primary key.
		$query = $this->_db->getQuery(true)
			->update($this->_tbl)
			->set($this->_db->quoteName($checkedOutField) . ' = ' . (int) $userId)
			->set($this->_db->quoteName($checkedOutTimeField) . ' = ' . $this->_db->quote($time));
		$this->appendPrimaryKeys($query, $pk);
		$this->_db->setQuery($query);
		$this->_db->execute();

		// Set table values in the object.
		$this->$checkedOutField      = (int) $userId;
		$this->$checkedOutTimeField = $time;

		// Post-processing by observers
		$event = AbstractEvent::create(
			'onTableAfterCheckout',
			[
				'subject'	=> $this,
				'userId'	=> $userId,
				'pk'		=> $pk,
			]
		);
		$this->getDispatcher()->dispatch('onTableAfterCheckout', $event);

		return true;
	}

	/**
	 * Method to check a row in if the necessary properties/fields exist.
	 *
	 * Checking a row in will allow other users the ability to edit the row.
	 *
	 * @param   mixed  $pk  An optional primary key value to check out.  If not set the instance property value is used.
	 *
	 * @return  boolean  True on success.
	 *
	 * @since   1.7.0
	 * @throws  \UnexpectedValueException
	 */
	public function checkIn($pk = null)
	{
		// Pre-processing by observers
		$event = AbstractEvent::create(
			'onTableBeforeCheckin',
			[
				'subject'	=> $this,
				'pk'		=> $pk,
			]
		);
		$this->getDispatcher()->dispatch('onTableBeforeCheckin', $event);

		// If there is no checked_out or checked_out_time field, just return true.
		if (!$this->hasField('checked_out') || !$this->hasField('checked_out_time'))
		{
			return true;
		}

		if (\is_null($pk))
		{
			$pk = array();

			foreach ($this->_tbl_keys as $key)
			{
				$pk[$this->$key] = $this->$key;
			}
		}
		elseif (!\is_array($pk))
		{
			$pk = array($this->_tbl_key => $pk);
		}

		foreach ($this->_tbl_keys as $key)
		{
			$pk[$key] = empty($pk[$key]) ? $this->$key : $pk[$key];

			if ($pk[$key] === null)
			{
				throw new \UnexpectedValueException('Null primary key not allowed.');
			}
		}

		// Get column names.
		$checkedOutField     = $this->getColumnAlias('checked_out');
		$checkedOutTimeField = $this->getColumnAlias('checked_out_time');

		$nullDate = $this->_supportNullValue ? 'NULL' : $this->_db->quote($this->_db->getNullDate());
		$nullID   = $this->_supportNullValue ? 'NULL' : '0';

		// Check the row in by primary key.
		$query = $this->_db->getQuery(true)
			->update($this->_tbl)
			->set($this->_db->quoteName($checkedOutField) . ' = ' . $nullID)
			->set($this->_db->quoteName($checkedOutTimeField) . ' = ' . $nullDate);
		$this->appendPrimaryKeys($query, $pk);
		$this->_db->setQuery($query);

		// Check for a database error.
		$this->_db->execute();

		// Set table values in the object.
		$this->$checkedOutField     = $this->_supportNullValue ? null : 0;
		$this->$checkedOutTimeField = $this->_supportNullValue ? null : '';

		// Post-processing by observers
		$event = AbstractEvent::create(
			'onTableAfterCheckin',
			[
				'subject'	=> $this,
				'pk'		=> $pk,
			]
		);
		$this->getDispatcher()->dispatch('onTableAfterCheckin', $event);

		Factory::getApplication()->triggerEvent('onAfterCheckin', array($this->_tbl));

		return true;
	}

	/**
	 * Validate that the primary key has been set.
	 *
	 * @return  boolean  True if the primary key(s) have been set.
	 *
	 * @since   3.1.4
	 */
	public function hasPrimaryKey()
	{
		if ($this->_autoincrement)
		{
			$empty = true;

			foreach ($this->_tbl_keys as $key)
			{
				$empty = $empty && empty($this->$key);
			}
		}
		else
		{
			$query = $this->_db->getQuery(true)
				->select('COUNT(*)')
				->from($this->_tbl);
			$this->appendPrimaryKeys($query);

			$this->_db->setQuery($query);
			$count = $this->_db->loadResult();

			if ($count == 1)
			{
				$empty = false;
			}
			else
			{
				$empty = true;
			}
		}

		return !$empty;
	}

	/**
	 * Method to increment the hits for a row if the necessary property/field exists.
	 *
	 * @param   mixed  $pk  An optional primary key value to increment. If not set the instance property value is used.
	 *
	 * @return  boolean  True on success.
	 *
	 * @since   1.7.0
	 * @throws  \UnexpectedValueException
	 */
	public function hit($pk = null)
	{
		// Pre-processing by observers
		$event = AbstractEvent::create(
			'onTableBeforeHit',
			[
				'subject'	=> $this,
				'pk'		=> $pk,
			]
		);
		$this->getDispatcher()->dispatch('onTableBeforeHit', $event);

		// If there is no hits field, just return true.
		if (!$this->hasField('hits'))
		{
			return true;
		}

		if (\is_null($pk))
		{
			$pk = array();

			foreach ($this->_tbl_keys as $key)
			{
				$pk[$key] = $this->$key;
			}
		}
		elseif (!\is_array($pk))
		{
			$pk = array($this->_tbl_key => $pk);
		}

		foreach ($this->_tbl_keys as $key)
		{
			$pk[$key] = \is_null($pk[$key]) ? $this->$key : $pk[$key];

			if ($pk[$key] === null)
			{
				throw new \UnexpectedValueException('Null primary key not allowed.');
			}
		}

		// Get column name.
		$hitsField = $this->getColumnAlias('hits');

		// Check the row in by primary key.
		$query = $this->_db->getQuery(true)
			->update($this->_tbl)
			->set($this->_db->quoteName($hitsField) . ' = (' . $this->_db->quoteName($hitsField) . ' + 1)');
		$this->appendPrimaryKeys($query, $pk);
		$this->_db->setQuery($query);
		$this->_db->execute();

		// Set table values in the object.
		$this->hits++;

		// Pre-processing by observers
		$event = AbstractEvent::create(
			'onTableAfterHit',
			[
				'subject'	=> $this,
				'pk'		=> $pk,
			]
		);
		$this->getDispatcher()->dispatch('onTableAfterHit', $event);

		return true;
	}

	/**
	 * Method to determine if a row is checked out and therefore uneditable by a user.
	 *
	 * If the row is checked out by the same user, then it is considered not checked out -- as the user can still edit it.
	 *
	 * @param   integer  $with     The user ID to preform the match with, if an item is checked out by this user the function will return false.
	 * @param   integer  $against  The user ID to perform the match against when the function is used as a static function.
	 *
	 * @return  boolean  True if checked out.
	 *
	 * @since   1.7.0
	 */
	public function isCheckedOut($with = 0, $against = null)
	{
		// Handle the non-static case.
		if (isset($this) && ($this instanceof Table) && \is_null($against))
		{
			$checkedOutField = $this->getColumnAlias('checked_out');
			$against = $this->get($checkedOutField);
		}

		// The item is not checked out or is checked out by the same user.
		if (!$against || ($against == $with))
		{
			return false;
		}

		// This last check can only be relied on if tracking session metadata
		if (Factory::getApplication()->get('session_metadata', true))
		{
			$db = Factory::getDbo();
			$query = $db->getQuery(true)
				->select('COUNT(userid)')
				->from($db->quoteName('#__session'))
				->where($db->quoteName('userid') . ' = ' . (int) $against);
			$db->setQuery($query);
			$checkedOut = (boolean) $db->loadResult();

			// If a session exists for the user then it is checked out.
			return $checkedOut;
		}

		// Assume if we got here that there is a value in the checked out column but it doesn't match the given user
		return true;
	}

	/**
	 * Method to get the next ordering value for a group of rows defined by an SQL WHERE clause.
	 *
	 * This is useful for placing a new item last in a group of items in the table.
	 *
	 * @param   string  $where  WHERE clause to use for selecting the MAX(ordering) for the table.
	 *
	 * @return  integer  The next ordering value.
	 *
	 * @since   1.7.0
	 * @throws  \UnexpectedValueException
	 */
	public function getNextOrder($where = '')
	{
		// Check if there is an ordering field set
		if (!$this->hasField('ordering'))
		{
			throw new \UnexpectedValueException(sprintf('%s does not support ordering.', \get_class($this)));
		}

		// Get the largest ordering value for a given where clause.
		$query = $this->_db->getQuery(true)
			->select('MAX(' . $this->_db->quoteName($this->getColumnAlias('ordering')) . ')')
			->from($this->_tbl);

		if ($where)
		{
			$query->where($where);
		}

		$this->_db->setQuery($query);
		$max = (int) $this->_db->loadResult();

		// Return the largest ordering value + 1.
		return $max + 1;
	}

	/**
	 * Get the primary key values for this table using passed in values as a default.
	 *
	 * @param   array  $keys  Optional primary key values to use.
	 *
	 * @return  array  An array of primary key names and values.
	 *
	 * @since   3.1.4
	 */
	public function getPrimaryKey(array $keys = array())
	{
		foreach ($this->_tbl_keys as $key)
		{
			if (!isset($keys[$key]))
			{
				if (!empty($this->$key))
				{
					$keys[$key] = $this->$key;
				}
			}
		}

		return $keys;
	}

	/**
	 * Method to compact the ordering values of rows in a group of rows defined by an SQL WHERE clause.
	 *
	 * @param   string  $where  WHERE clause to use for limiting the selection of rows to compact the ordering values.
	 *
	 * @return  mixed  Boolean  True on success.
	 *
	 * @since   1.7.0
	 * @throws  \UnexpectedValueException
	 */
	public function reorder($where = '')
	{
		// Check if there is an ordering field set
		if (!$this->hasField('ordering'))
		{
			throw new \UnexpectedValueException(sprintf('%s does not support ordering.', \get_class($this)));
		}

		$quotedOrderingField = $this->_db->quoteName($this->getColumnAlias('ordering'));

		$subquery = $this->_db->getQuery(true)
			->from($this->_tbl)
			->selectRowNumber($quotedOrderingField, 'new_ordering');

		$query = $this->_db->getQuery(true)
			->update($this->_tbl)
			->set($quotedOrderingField . ' = sq.new_ordering');

		$innerOn = array();

		// Get the primary keys for the selection.
		foreach ($this->_tbl_keys as $i => $k)
		{
			$subquery->select($this->_db->quoteName($k, 'pk__' . $i));
			$innerOn[] = $this->_db->quoteName($k) . ' = sq.' . $this->_db->quoteName('pk__' . $i);
		}

		// Setup the extra where and ordering clause data.
		if ($where)
		{
			$subquery->where($where);
			$query->where($where);
		}

		$subquery->where($quotedOrderingField . ' >= 0');
		$query->where($quotedOrderingField . ' >= 0');
		$query->innerJoin('(' . (string) $subquery . ') AS sq ');

		foreach ($innerOn as $key)
		{
			$query->where($key);
		}

		// Pre-processing by observers
		$event = AbstractEvent::create(
			'onTableBeforeReorder',
			[
				'subject'	=> $this,
				'query'		=> $query,
				'where'		=> $where,
			]
		);
		$this->getDispatcher()->dispatch('onTableBeforeReorder', $event);

		$this->_db->setQuery($query);
		$this->_db->execute();

		// Post-processing by observers
		$event = AbstractEvent::create(
			'onTableAfterReorder',
			[
				'subject'	=> $this,
				'where'		=> $where,
			]
		);
		$this->getDispatcher()->dispatch('onTableAfterReorder', $event);

		return true;
	}

	/**
	 * Method to move a row in the ordering sequence of a group of rows defined by an SQL WHERE clause.
	 *
	 * Negative numbers move the row up in the sequence and positive numbers move it down.
	 *
	 * @param   integer  $delta  The direction and magnitude to move the row in the ordering sequence.
	 * @param   string   $where  WHERE clause to use for limiting the selection of rows to compact the ordering values.
	 *
	 * @return  boolean  True on success.
	 *
	 * @since   1.7.0
	 * @throws  \UnexpectedValueException
	 */
	public function move($delta, $where = '')
	{
		// Check if there is an ordering field set
		if (!$this->hasField('ordering'))
		{
			throw new \UnexpectedValueException(sprintf('%s does not support ordering.', \get_class($this)));
		}

		$orderingField       = $this->getColumnAlias('ordering');
		$quotedOrderingField = $this->_db->quoteName($orderingField);

		// If the change is none, do nothing.
		if (empty($delta))
		{
			return true;
		}

		$row   = null;
		$query = $this->_db->getQuery(true);

		// Select the primary key and ordering values from the table.
		$query->select(implode(',', $this->_tbl_keys) . ', ' . $quotedOrderingField)
			->from($this->_tbl);

		// If the movement delta is negative move the row up.
		if ($delta < 0)
		{
			$query->where($quotedOrderingField . ' < ' . (int) $this->$orderingField)
				->order($quotedOrderingField . ' DESC');
		}
		// If the movement delta is positive move the row down.
		elseif ($delta > 0)
		{
			$query->where($quotedOrderingField . ' > ' . (int) $this->$orderingField)
				->order($quotedOrderingField . ' ASC');
		}

		// Add the custom WHERE clause if set.
		if ($where)
		{
			$query->where($where);
		}

		// Pre-processing by observers
		$event = AbstractEvent::create(
			'onTableBeforeMove',
			[
				'subject'	=> $this,
				'query'		=> $query,
				'delta'		=> $delta,
				'where'		=> $where,
			]
		);
		$this->getDispatcher()->dispatch('onTableBeforeMove', $event);

		// Select the first row with the criteria.
		$query->setLimit(1);
		$this->_db->setQuery($query);
		$row = $this->_db->loadObject();

		// If a row is found, move the item.
		if (!empty($row))
		{
			// Update the ordering field for this instance to the row's ordering value.
			$query->clear()
				->update($this->_tbl)
				->set($quotedOrderingField . ' = ' . (int) $row->$orderingField);
			$this->appendPrimaryKeys($query);
			$this->_db->setQuery($query);
			$this->_db->execute();

			// Update the ordering field for the row to this instance's ordering value.
			$query->clear()
				->update($this->_tbl)
				->set($quotedOrderingField . ' = ' . (int) $this->$orderingField);
			$this->appendPrimaryKeys($query, $row);
			$this->_db->setQuery($query);
			$this->_db->execute();

			// Update the instance value.
			$this->$orderingField = $row->$orderingField;
		}
		else
		{
			// Update the ordering field for this instance.
			$query->clear()
				->update($this->_tbl)
				->set($quotedOrderingField . ' = ' . (int) $this->$orderingField);
			$this->appendPrimaryKeys($query);
			$this->_db->setQuery($query);
			$this->_db->execute();
		}

		// Post-processing by observers
		$event = AbstractEvent::create(
			'onTableAfterMove',
			[
				'subject'	=> $this,
				'row'		=> $row,
				'delta'		=> $delta,
				'where'		=> $where,
			]
		);
		$this->getDispatcher()->dispatch('onTableAfterMove', $event);

		return true;
	}

	/**
	 * Method to set the publishing state for a row or list of rows in the database table.
	 *
	 * The method respects checked out rows by other users and will attempt to checkin rows that it can after adjustments are made.
	 *
	 * @param   mixed    $pks     An optional array of primary key values to update. If not set the instance property value is used.
	 * @param   integer  $state   The publishing state. eg. [0 = unpublished, 1 = published]
	 * @param   integer  $userId  The user ID of the user performing the operation.
	 *
	 * @return  boolean  True on success; false if $pks is empty.
	 *
	 * @since   1.7.0
	 */
	public function publish($pks = null, $state = 1, $userId = 0)
	{
		// Sanitize input
		$userId = (int) $userId;
		$state  = (int) $state;

		// Pre-processing by observers
		$event = AbstractEvent::create(
			'onTableBeforePublish',
			[
				'subject'	=> $this,
				'pks'		=> $pks,
				'state'		=> $state,
				'userId'	=> $userId,
			]
		);
		$this->getDispatcher()->dispatch('onTableBeforePublish', $event);

		if (!\is_null($pks))
		{
			if (!\is_array($pks))
			{
				$pks = array($pks);
			}

			foreach ($pks as $key => $pk)
			{
				if (!\is_array($pk))
				{
					$pks[$key] = array($this->_tbl_key => $pk);
				}
			}
		}

		// If there are no primary keys set check to see if the instance key is set.
		if (empty($pks))
		{
			$pk = array();

			foreach ($this->_tbl_keys as $key)
			{
				if ($this->$key)
				{
					$pk[$key] = $this->$key;
				}
				// We don't have a full primary key - return false
				else
				{
					$this->setError(Text::_('JLIB_DATABASE_ERROR_NO_ROWS_SELECTED'));

					return false;
				}
			}

			$pks = array($pk);
		}

		$publishedField = $this->getColumnAlias('published');
		$checkedOutField = $this->getColumnAlias('checked_out');

		foreach ($pks as $pk)
		{
			// Update the publishing state for rows with the given primary keys.
			$query = $this->_db->getQuery(true)
				->update($this->_tbl)
				->set($this->_db->quoteName($publishedField) . ' = ' . (int) $state);

			// If publishing, set published date/time if not previously set
			if ($state && $this->hasField('publish_up') && (int) $this->publish_up == 0)
			{
				$nowDate = $this->_db->quote(Factory::getDate()->toSql());
				$query->set($this->_db->quoteName($this->getColumnAlias('publish_up')) . ' = ' . $nowDate);
			}

			// Determine if there is checkin support for the table.
			if ($this->hasField('checked_out') || $this->hasField('checked_out_time'))
			{
				$query->where(
					'('
						. $this->_db->quoteName($checkedOutField) . ' = 0'
						. ' OR ' . $this->_db->quoteName($checkedOutField) . ' = ' . (int) $userId
						. ' OR ' . $this->_db->quoteName($checkedOutField) . ' IS NULL'
					. ')'
				);
				$checkin = true;
			}
			else
			{
				$checkin = false;
			}

			// Build the WHERE clause for the primary keys.
			$this->appendPrimaryKeys($query, $pk);

			$this->_db->setQuery($query);

			try
			{
				$this->_db->execute();
			}
			catch (\RuntimeException $e)
			{
				$this->setError($e->getMessage());

				return false;
			}

			// If checkin is supported and all rows were adjusted, check them in.
			if ($checkin && (\count($pks) == $this->_db->getAffectedRows()))
			{
				$this->checkIn($pk);
			}

			// If the Table instance value is in the list of primary keys that were set, set the instance.
			$ours = true;

			foreach ($this->_tbl_keys as $key)
			{
				if ($this->$key != $pk[$key])
				{
					$ours = false;
				}
			}

			if ($ours)
			{
				$this->$publishedField = $state;
			}
		}

		$this->setError('');

		// Pre-processing by observers
		$event = AbstractEvent::create(
			'onTableAfterPublish',
			[
				'subject'	=> $this,
				'pks'		=> $pks,
				'state'		=> $state,
				'userId'	=> $userId,
			]
		);
		$this->getDispatcher()->dispatch('onTableAfterPublish', $event);

		return true;
	}

	/**
	 * Method to lock the database table for writing.
	 *
	 * @return  boolean  True on success.
	 *
	 * @since   1.7.0
	 * @throws  \RuntimeException
	 */
	protected function _lock()
	{
		$this->_db->lockTable($this->_tbl);
		$this->_locked = true;

		return true;
	}

	/**
	 * Method to return the real name of a "special" column such as ordering, hits, published
	 * etc etc. In this way you are free to follow your db naming convention and use the
	 * built in \Joomla functions.
	 *
	 * @param   string  $column  Name of the "special" column (ie ordering, hits)
	 *
	 * @return  string  The string that identify the special
	 *
	 * @since   3.4
	 */
	public function getColumnAlias($column)
	{
		// Get the column data if set
		if (isset($this->_columnAlias[$column]))
		{
			$return = $this->_columnAlias[$column];
		}
		else
		{
			$return = $column;
		}

		// Sanitize the name
		$return = preg_replace('#[^A-Z0-9_]#i', '', $return);

		return $return;
	}

	/**
	 * Method to register a column alias for a "special" column.
	 *
	 * @param   string  $column       The "special" column (ie ordering)
	 * @param   string  $columnAlias  The real column name (ie foo_ordering)
	 *
	 * @return  void
	 *
	 * @since   3.4
	 */
	public function setColumnAlias($column, $columnAlias)
	{
		// Sanitize the column name alias
		$column = strtolower($column);
		$column = preg_replace('#[^A-Z0-9_]#i', '', $column);

		// Set the column alias internally
		$this->_columnAlias[$column] = $columnAlias;
	}

	/**
	 * Method to unlock the database table for writing.
	 *
	 * @return  boolean  True on success.
	 *
	 * @since   1.7.0
	 */
	protected function _unlock()
	{
		if ($this->_locked)
		{
			$this->_db->unlockTables();
			$this->_locked = false;
		}

		return true;
	}

	/**
	 * Check if the record has a property (applying a column alias if it exists)
	 *
	 * @param   string  $key  key to be checked
	 *
	 * @return  boolean
	 *
	 * @since   3.9.11
	 */
	public function hasField($key)
	{
		$key = $this->getColumnAlias($key);

		return property_exists($this, $key);
	}
}
