<?php

/**
 * @file classes/core/DataObject.inc.php
 *
 * Copyright (c) 2014-2021 Simon Fraser University
 * Copyright (c) 2000-2021 John Willinsky
 * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
 *
 * @class DataObject
 * @ingroup core
 * @see Core
 *
 * @brief Any class with an associated DAO should extend this class.
 */


class DataObject {
	/** @var array Array of object data */
	var $_data = array();

	/** @var boolean whether this objects loads meta-data adapters from the database */
	var $_hasLoadableAdapters = false;

	/** @var array an array of meta-data extraction adapters (one per supported schema) */
	var $_metadataExtractionAdapters = array();

	/** @var boolean whether extraction adapters have already been loaded from the database */
	var $_extractionAdaptersLoaded = false;

	/** @var array an array of meta-data injection adapters (one per supported schema) */
	var $_metadataInjectionAdapters = array();

	/** @var boolean whether injection adapters have already been loaded from the database */
	var $_injectionAdaptersLoaded = false;

	/**
	 * Constructor
	 */
	public function __construct() { }


	//
	// Getters and Setters
	//
	/**
	 * Get a piece of data for this object, localized to the current
	 * locale if possible.
	 * @param $key string
	 * @param $preferredLocale string
	 * @return mixed
	 */
	public function getLocalizedData($key, $preferredLocale = null) {
		$localePrecedence = AppLocale::getLocalePrecedence();
		foreach ($localePrecedence as $locale) {
			$value =& $this->getData($key, $locale);
			if (!empty($value)) return $value;
			unset($value);
		}

		// Fallback: Get the first available piece of data.
		$data =& $this->getData($key, null);
		if (!empty($data)) {
			// WARNING: Collapsing the following into a single line causes PHP 5.0.5 to die.
			$locales = array_keys($data);
			$firstLocale = array_shift($locales);
			return $data[$firstLocale];
		}

		// No data available; return null.
		unset($data);
		$data = null;
		return $data;
	}

	/**
	 * Get the value of a data variable.
	 * @param $key string
	 * @param $locale string (optional)
	 * @return mixed
	 */
	public function &getData($key, $locale = null) {
		if (is_null($locale)) {
			if (array_key_exists($key, $this->_data)) {
				return $this->_data[$key];
			}
		} else {
			// see http://bugs.php.net/bug.php?id=29848
			if (array_key_exists($key, $this->_data) && is_array($this->_data[$key]) && array_key_exists($locale, $this->_data[$key])) {
				return $this->_data[$key][$locale];
			}
		}
		$nullVar = null;
		return $nullVar;
	}

	/**
	 * Set the value of a new or existing data variable.
	 * NB: Passing in null as a value will unset the
	 * data variable if it already existed.
	 * @param $key string
	 * @param $value mixed can be either a single value or
	 *  an array of of localized values in the form:
	 *   array(
	 *     'fr_FR' => 'en français',
	 *     'en_US' => 'in English',
	 *     ...
	 *   )
	 * @param $locale string (optional) non-null for a single
	 *  localized value. Null for a non-localized value or
	 *  when setting all locales at once (see comment for
	 *  $value parameter)
	 */
	public function setData($key, $value, $locale = null) {
		if (is_null($locale)) {
			// This is either a non-localized value or we're
			// passing in all locales at once.
			if (is_null($value)) {
				if (array_key_exists($key, $this->_data)) unset($this->_data[$key]);
			} else {
				$this->_data[$key] = $value;
			}
		} else {
			// (Un-)set a single localized value.
			if (is_null($value)) {
				// see http://bugs.php.net/bug.php?id=29848
				if (array_key_exists($key, $this->_data)) {
					if (is_array($this->_data[$key]) && array_key_exists($locale, $this->_data[$key])) unset($this->_data[$key][$locale]);
					// Was this the last entry for the data variable?
					if (empty($this->_data[$key])) unset($this->_data[$key]);
				}
			} else {
				$this->_data[$key][$locale] = $value;
			}
		}
	}

	/**
	 * Unset an element of the data object.
	 * @param $key string
	 * @param $locale string (optional) non-null for a single
	 *  localized value. Null for a non-localized value or
	 *  when unsetting all locales at once.
	 */
	public function unsetData($key, $locale = null) {
		if (is_null($locale)) {
			unset($this->_data[$key]);
		} else {
			unset($this->_data[$key][$locale]);
		}
	}

	/**
	 * Check whether a value exists for a given data variable.
	 * @param $key string
	 * @param $locale string (optional)
	 * @return boolean
	 */
	public function hasData($key, $locale = null) {
		if (is_null($locale)) {
			return array_key_exists($key, $this->_data);
		} else {
			// see http://bugs.php.net/bug.php?id=29848
			return array_key_exists($key, $this->_data) && is_array($this->_data[$key]) && array_key_exists($locale, $this->_data[$key]);
		}
	}

	/**
	 * Return an array with all data variables.
	 * @return array
	 */
	public function &getAllData() {
		return $this->_data;
	}

	/**
	 * Set all data variables at once.
	 * @param $data array
	 */
	public function setAllData($data) {
		$this->_data = $data;
	}

	/**
	 * Get ID of object.
	 * @return int
	 */
	public function getId() {
		return $this->getData('id');
	}

	/**
	 * Set ID of object.
	 * @param $id int
	 */
	public function setId($id) {
		$this->setData('id', $id);
	}


	//
	// Public helper methods
	//
	/**
	 * Upcast this data object to the target object.
	 *
	 * We use the DataObject's getAllData() and setAllData() interface
	 * to upcast objects. This means that if the default implementations
	 * of these methods do not provide data that is adequate for
	 * upcasting between objects of the same inheritance hierarchy
	 * then objects that need more complicated casting behavior
	 * must override these methods.
	 *
	 * Note: Data in the target object will be overwritten. We do not
	 * clone the target object before we upcast.
	 *
	 * @param $targetObject DataObject The object to cast to.
	 *
	 * @return DataObject The upcast target object.
	 */
	public function upcastTo($targetObject) {
		// Copy data from the source to the target.
		$targetObject->setAllData($this->getAllData());

		// Return the upcast object.
		return $targetObject;
	}


	//
	// MetadataProvider interface implementation
	//
	/**
	 * Set whether the object has loadable meta-data adapters
	 * @param $hasLoadableAdapters boolean
	 */
	public function setHasLoadableAdapters($hasLoadableAdapters) {
		$this->_hasLoadableAdapters = $hasLoadableAdapters;
	}

	/**
	 * Get whether the object has loadable meta-data adapters
	 * @return boolean
	 */
	public function getHasLoadableAdapters() {
		return $this->_hasLoadableAdapters;
	}

	/**
	 * Add a meta-data adapter that will be supported
	 * by this application entity. Only one adapter per schema
	 * can be added.
	 * @param $metadataAdapter MetadataDataObjectAdapter
	 */
	public function addSupportedMetadataAdapter($metadataAdapter) {
		$metadataSchemaName = $metadataAdapter->getMetadataSchemaName();
		assert(!empty($metadataSchemaName));

		// NB: Some adapters are injectors and extractors at the same time,
		// notably the meta-data description dummy adapter that converts
		// from/to a meta-data description. That's why we have to check
		// input and output type separately.

		// Is this a meta-data extractor?
		$inputType = $metadataAdapter->getInputType();
		if ($inputType->checkType($this)) {
			if (!isset($this->_metadataExtractionAdapters[$metadataSchemaName])) {
				$this->_metadataExtractionAdapters[$metadataSchemaName] = $metadataAdapter;
			}
		}

		// Is this a meta-data injector?
		$outputType = $metadataAdapter->getOutputType();
		if ($outputType->checkType($this)) {
			if (!isset($this->_metadataInjectionAdapters[$metadataSchemaName])) {
				$this->_metadataInjectionAdapters[$metadataSchemaName] = $metadataAdapter;
			}
		}
	}

	/**
	 * Remove all adapters for the given meta-data schema
	 * (if it exists).
	 *
	 * @param $metadataSchemaName string fully qualified class name
	 * @return boolean true if an adapter was removed, otherwise false.
	 */
	public function removeSupportedMetadataAdapter($metadataSchemaName) {
		$result = false;
		if (isset($this->_metadataExtractionAdapters[$metadataSchemaName])) {
			unset($this->_metadataExtractionAdapters[$metadataSchemaName]);
			$result = true;
		}
		if (isset($this->_metadataInjectionAdapters[$metadataSchemaName])) {
			unset($this->_metadataInjectionAdapters[$metadataSchemaName]);
			$result = true;
		}
		return $result;
	}

	/**
	 * Get all meta-data extraction adapters that
	 * support this data object. This includes adapters
	 * loaded from the database.
	 * @return array
	 */
	public function getSupportedExtractionAdapters() {
		// Load meta-data adapters from the database.
		if ($this->getHasLoadableAdapters() && !$this->_extractionAdaptersLoaded) {
			$filterDao = DAORegistry::getDAO('FilterDAO'); /* @var $filterDao FilterDAO */
			$loadedAdapters = $filterDao->getObjectsByTypeDescription('class::%', 'metadata::%', $this);
			foreach($loadedAdapters as $loadedAdapter) {
				$this->addSupportedMetadataAdapter($loadedAdapter);
			}
			$this->_extractionAdaptersLoaded = true;
		}

		return $this->_metadataExtractionAdapters;
	}

	/**
	 * Get all meta-data injection adapters that
	 * support this data object. This includes adapters
	 * loaded from the database.
	 * @return array
	 */
	public function getSupportedInjectionAdapters() {
		// Load meta-data adapters from the database.
		if ($this->getHasLoadableAdapters() && !$this->_injectionAdaptersLoaded) {
			$filterDao = DAORegistry::getDAO('FilterDAO'); /* @var $filterDao FilterDAO */
			$loadedAdapters = $filterDao->getObjectsByTypeDescription('metadata::%', 'class::%', $this, false);
			foreach($loadedAdapters as $loadedAdapter) {
				$this->addSupportedMetadataAdapter($loadedAdapter);
			}
			$this->_injectionAdaptersLoaded = true;
		}

		return $this->_metadataInjectionAdapters;
	}

	/**
	 * Returns all supported meta-data schemas
	 * which are supported by extractor adapters.
	 * @return array
	 */
	public function getSupportedMetadataSchemas() {
		$supportedMetadataSchemas = array();
		$extractionAdapters = $this->getSupportedExtractionAdapters();
		foreach($extractionAdapters as $metadataAdapter) {
			$supportedMetadataSchemas[] = $metadataAdapter->getMetadataSchema();
		}
		return $supportedMetadataSchemas;
	}

	/**
	 * Retrieve the names of meta-data
	 * properties of this data object.
	 * @param $translated boolean if true, return localized field
	 *  names, otherwise return additional field names.
	 */
	public function getMetadataFieldNames($translated = true) {
		// Create a list of all possible meta-data field names
		$metadataFieldNames = array();
		$extractionAdapters = $this->getSupportedExtractionAdapters();
		foreach($extractionAdapters as $metadataAdapter) {
			// Add the field names from the current adapter
			$metadataFieldNames = array_merge(
				$metadataFieldNames,
				$metadataAdapter->getDataObjectMetadataFieldNames($translated)
			);
		}
		return array_unique($metadataFieldNames);
	}

	/**
	 * Retrieve the names of meta-data
	 * properties that need to be persisted
	 * (i.e. that have data).
	 * @param $translated boolean if true, return localized field
	 *  names, otherwise return additional field names.
	 * @return array an array of field names
	 */
	public function getSetMetadataFieldNames($translated = true) {
		// Retrieve a list of all possible meta-data field names
		$metadataFieldNameCandidates = $this->getMetadataFieldNames($translated);

		// Only retain those fields that have data
		$metadataFieldNames = array();
		foreach($metadataFieldNameCandidates as $metadataFieldNameCandidate) {
			if($this->hasData($metadataFieldNameCandidate)) {
				$metadataFieldNames[] = $metadataFieldNameCandidate;
			}
		}
		return $metadataFieldNames;
	}

	/**
	 * Retrieve the names of translated meta-data
	 * properties that need to be persisted.
	 * @return array an array of field names
	 */
	public function getLocaleMetadataFieldNames() {
		return $this->getMetadataFieldNames(true);
	}

	/**
	 * Retrieve the names of additional meta-data
	 * properties that need to be persisted.
	 * @return array an array of field names
	 */
	public function getAdditionalMetadataFieldNames() {
		return $this->getMetadataFieldNames(false);
	}

	/**
	 * Inject a meta-data description into this
	 * data object.
	 * @param $metadataDescription MetadataDescription
	 * @param $replace boolean whether to delete existing meta-data
	 * @return boolean true on success, otherwise false
	 */
	public function injectMetadata($metadataDescription) {
		$dataObject = null;
		$metadataSchemaName = $metadataDescription->getMetadataSchemaName();
		$injectionAdapters = $this->getSupportedInjectionAdapters();
		if(isset($injectionAdapters[$metadataSchemaName])) {
			// Get the meta-data adapter that supports the
			// given meta-data description's schema.
			$metadataAdapter = $injectionAdapters[$metadataSchemaName]; /* @var $metadataAdapter MetadataDataObjectAdapter */

			// Pass in a reference to the data object which
			// the filter will use to update the current instance
			// of the data object.
			$metadataAdapter->setTargetDataObject($this);

			// Use adapter filter to convert from a meta-data
			// description to a data object.
			$dataObject = $metadataAdapter->execute($metadataDescription);
		}
		return $dataObject;
	}

	/**
	 * Extract a meta-data description from this
	 * data object.
	 * @param $metadataSchema MetadataSchema
	 * @return $metadataDescription MetadataDescription
	 */
	public function extractMetadata($metadataSchema) {
		$metadataDescription = null;
		$metadataSchemaName = $metadataSchema->getClassName();
		$extractionAdapters = $this->getSupportedExtractionAdapters();
		if(isset($extractionAdapters[$metadataSchemaName])) {
			// Get the meta-data adapter that supports the
			// given meta-data description's schema.
			$metadataAdapter = $extractionAdapters[$metadataSchemaName];

			// Use adapter filter to convert from a data object
			// to a meta-data description.
			$metadataDescription = $metadataAdapter->execute($this);
		}
		return $metadataDescription;
	}

	/**
	 * Get DAO class name for this object.
	 * @return DAO
	 */
	public function getDAO() {
		assert(false);
	}
}

