<?php
/**
 * This file is part of the set of generic classes available for implementations
 * of the DDR Simple API to speed up implementation development. It contains a
 * generic abstract implementation of the {@link Service} interface.
 *
 * @author Sylvain Lequeux
 * @author Francois Daoust <fd@w3.org>
 * @package AskPythia
 * @subpackage Implementation
 * @version $Revision: 1.17 $
 * @license http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231.html W3C Software Notice and License
 * @copyright Copyright (c) 2009, W3C (MIT, ERCIM, Keio)
 */

/**
 * Include the {@link Service} interface definition.
 */
require_once(dirname(__FILE__).'/../../interface/service.php');
/**
 * Include the {@link NameException} class definition as
 * factory methods raise such exceptions when something is wrong.
 */
require_once(dirname(__FILE__)."/../../interface/nameException.php");
/**
 * Include the {@link SystemException} class definition as
 * factory methods raise such exceptions when arguments are invalid.
 */
require_once(dirname(__FILE__)."/../../interface/systemException.php");

/**
 * Include the {@link BasicEvidence} class implementation.
 */
require_once(dirname(__FILE__).'/../basic/basicEvidence.php');
/**
 * Include the {@link BasicPropertyName} class implementation.
 */
require_once(dirname(__FILE__).'/../basic/basicPropertyName.php');
/**
 * Include the {@link BasicPropertyRef} class implementation.
 */
require_once(dirname(__FILE__).'/../basic/basicPropertyRef.php');
/**
 * Include the {@link BasicPropertyValue} class implementation.
 */
require_once(dirname(__FILE__).'/../basic/basicPropertyValue.php');
/**
 * Include the {@link BasicPropertyValues} class implementation.
 */
require_once(dirname(__FILE__).'/../basic/basicPropertyValues.php');


/**
 * The basic {@link Service} class is an abstract implementation of the
 * underlying Service interface that features:
 * - factory methods based on the other basic classes
 * - implementations of most query methods from a single {@link getPropertyValues()}
 *   method that must be implemented in the concrete subclass 
 * - methods arguments verification ({@link SystemException} exceptions are raised
 *   according to the DDR Simple API standard)
 * - property/aspect/vocabulary verification based on the
 *   {@link $supportedProperties} that must be set in the concrete implementation
 *   of the {@link initializeProperties()} method. 
 *
 * Two methods must be implemented in the concrete subclass:
 * - {@link initializeProperties()} to initialize the list of properties
 *   supported by the implementation, as well as global implementation specific
 *   properties
 * - {@link getPropertyValues()} that is defined as identical to
 *   {@link Service::getPropertyValues()}, except that it is only visible
 *   internally and is guaranteed to receive a valid {@link Evidence} instance.
 * 
 * Two additional methods should be overridden in subclasses:
 * - {@link getImplementationVersion()} to return information about the
 *   implementation of the API including the current version. Basic implementation
 *   returns "W3C basic DDR Simple API".
 * - {@link getDataVersion()} to return information about the underlying data.
 *   Basic implementation returns "__NOT_SUPPORTED" in accordance with the spec.
 * 
 * The {@link newHTTPEvidenceM()} method creates an evidence from the list of
 * HTTP header names and values. The method may need to be overridden in
 * subclasses for more specific needs.
 * 
 * For performance reasons, it may be a good idea to re-implement some of the
 * query methods in the concrete subclass. Arguments and properties verification
 * may not be performance-friendly either. Implementations that need to speed up
 * things as much as possible may want to consider a direct implementation of
 * the {@link Service} interface. 
 *
 * @author Sylvain Lequeux
 * @author Francois Daoust <fd@w3.org>
 * @package AskPythia
 * @subpackage Implementation
 * @link http://www.w3.org/TR/DDR-Simple-API/ Device Description Repository Simple API
 * @version $Revision: 1.17 $
 * @license http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231.html W3C Software Notice and License
 * @copyright Copyright (c) 2009, W3C (MIT, ERCIM, Keio)
 */
abstract class BasicService implements Service {
	/**
	 * The complete list of properties supported by the implementation is
	 * represented as a tree.
	 *
	 * The first level of the tree is the vocabulary IRI. The second level
	 * contains the local property names supported within each vocabulary. The
	 * third level contains an array of parameters that define the local
	 * properties. Implementations may complete the array with more specific
	 * parameters, 'aspects' is the only mandatory parameter. It must contain
	 * the list of aspects the property may apply to. The first aspect in the
	 * list must be the default aspect for the property.
	 *
	 * The list must be initialized in the concrete {@link initialize()} method
	 * of the subclass that extends this abstract class.
	 * 
	 * Example:
	 *  array(
	 *    'http://example.org' => array(
	 *      'coolCapability' => array(
	 *        'aspects' => array('defaultAspect', 'otherAspect')
	 *      ),
	 *      'otherCoolCapability' => array(
	 *        'aspects' => array('webBrowser')
	 *      ),
	 *    ),
	 *    'http://example.com' => array(
	 *      'veryCoolCapability' => array(
	 *        'aspects' => array('device')
	 *      ),
	 *    ),
	 *  )     
	 *
	 * @var array(string=>array(string=>array(string=>mixed))) The complete "tree" of properties.
	 */
	protected $supportedProperties;

	/**
	 * @var string the default vocabulary IRI that will be used when querying
	 *             the underlying database.
	 */
	private $defaultVocabulary;

	
	/**
	 * Abstract method called by all query methods and that returns all
	 * available property values for all the aspects and vocabularies
	 * known by the underlying implementation for the given evidence.
	 * 
	 * This method must be implemented in the concrete subclass.
	 * 
	 * @param Evidence $evidence Evidence to use to identify the device in the DDR.
	 * @return PropertyValues all the property values that match the evidence.
	 */
	abstract protected function getPropertyValues($evidence);
	
	/**
	 * Abstract method called by the {@link initialize()} method to set
	 * the list of supported properties and implementation-specific
	 * settings.
	 * 
	 * This method must be implemented in the concrete subclass.
	 *  
	 * @param mixed $props key/value Implementation-specific settings.
	 * @exception InitializationException there was a problem during initialization.
	 */
	abstract protected function initializeProperties($props);
	
	
	/**
	 * Returns a basic string that identifies this basic implementation,
	 * along with the version of the file in the CVS repository.
	 * 
	 * This method should be overridden in the concrete subclass.
	 *
	 * @return string The implementation version.
	 */
	public function getImplementationVersion(){
		$cvsRevision = '$Revision: 1.17 $';
		$versionStart = strlen('$Revision: ');
		$versionEnd = strpos($cvsRevision, ' ', $versionStart);
		$version = substr($cvsRevision, $versionStart, $versionEnd - $versionStart);
		$implementation = 'W3C basic implementation of the DDR Simple API in PHP, v' . $version;  
		return $implementation;
	}

	/**
	 * Returns "__NOT_SUPPORTED" as this class is abstract and agnostic of the
	 * data repository that will be used in the concrete class.
	 * 
	 * This method should be overridden in the concrete subclass.
	 *
	 * @return string "__NOT_SUPPORTED"
	 */
	public function getDataVersion(){
		return '__NOT_SUPPORTED';
	}

	
	public function newHTTPEvidence(){
		return new BasicEvidence();
	}

	/**
	 * Creates an HTTP Evidence from the given list of HTTP header
	 * name and value pairs.
	 *
	 * The Evidence will be used to select a device in the underlying
	 * repository.
	 * 
	 * Please note that the method does not check that the given keys
	 * are real HTTP headers, and could therefore be used to create
	 * an evidence out of a random list of keys. The important point
	 * to note is that keys are treated in a case insensitive manner.  
	 * 
	 * @param array(string=>string) $map the HTTP header names and values to
	 *                                   use as evidence.
	 * @return Evidence an HTTP Evidence.
	 */
	public function newHTTPEvidenceM($request){
		if(!isset($request)){
			throw new SystemException(
				'Cannot create an Evidence from a null set of HTTP headers.',
				SystemException::$ILLEGAL_ARGUMENT);
		}
		
		$evidence = new BasicEvidence();
		foreach($request as $name=>$value){
			if(!isset($name) || !isset($value)){
				throw new SystemException(
					'Cannot create an Evidence from a null HTTP name or value.',
					SystemException::$ILLEGAL_ARGUMENT);
			}
			$evidence->put(strtolower($name), $value);
		}
		
		return $evidence;
	}

	public function newPropertyNameS($localPropertyName){
		$this->checkPropertyWithoutAspect($localPropertyName, $this->defaultVocabulary);
		
		$propName = new BasicPropertyName($this->defaultVocabulary, $localPropertyName);
		return $propName;
	}

	public function newPropertyNameSS($localPropertyName, $vocabularyIRI){
		$this->checkPropertyWithoutAspect($localPropertyName, $vocabularyIRI);

		$propName = new BasicPropertyName($vocabularyIRI, $localPropertyName);
		return $propName;
	}

	public function newPropertyRefS($localPropertyName){
		$propName = $this->newPropertyNameSS($localPropertyName, $this->defaultVocabulary);
		$aspectName = $this->getDefaultAspectName($localPropertyName, $this->defaultVocabulary);
		$propRef = new BasicPropertyRef($propName, $aspectName);
		return $propRef;
	}

	public function newPropertyRefPn($propertyName){
		$this->checkPropertyName($propertyName);

		$aspectName = $this->getDefaultAspectName(
			$propertyName->getLocalPropertyName(),
			$propertyName->getNamespace());
			
		$propRef = new BasicPropertyRef($propertyName, $aspectName);
		return $propRef;
	}

	public function newPropertyRefPnS($propertyName, $localAspectName){
		// Check property name and aspect name
		$this->checkPropertyName($propertyName);
		$this->checkAspectName($localAspectName);
		// TODO: check aspect name against property name

		$propRef = new BasicPropertyRef($propertyName, $localAspectName);
		return $propRef;
	}

	public function getPropertyValuesE($evidence){
		$this->checkEvidence($evidence);
		
		$result = $this->getPropertyValues($evidence);
		return $result;
	}

	public function getPropertyValuesES($evidence, $localAspectName){
		$this->checkEvidence($evidence);
		$this->checkAspectName($localAspectName);

		$properties = $this->getPropertyValues($evidence)->getAll();
		$result = new BasicPropertyValues();
		foreach($properties as $property){
			$ref = $property->getPropertyRef();
			if(($ref->getAspectName() == $localAspectName)
			&& ($ref->getNamespace() == $this->defaultVocabulary)){
				$result->add($property);
			}
		}
		return $result;
	}

	public function getPropertyValuesESS($evidence, $localAspectName, $vocabularyIRI){
		// TODO: check aspect name against the vocabulary IRI
		$this->checkEvidence($evidence);
		$this->checkAspectName($localAspectName);
		$this->checkNamespace($vocabularyIRI);

		$properties = $this->getPropertyValues($evidence)->getAll();
		$result = new BasicPropertyValues();
		foreach($properties as $property){
			$ref = $property->getPropertyRef();
			if(($ref->getAspectName() == $localAspectName)
			&& ($ref->getNamespace() == $vocabularyIRI)) {
				$result->add($property);
			}
		}
		return $result;
	}

	public function getPropertyValuesEPr($evidence, $propertyRefs){
		$this->checkEvidence($evidence);
		
		$properties = $this->getPropertyValues($evidence)->getAll();
		$result = new BasicPropertyValues();

		foreach($propertyRefs as $propRef){
			$found = false;
			foreach($properties as $prop){
				$ref = $prop->getPropertyRef();
				if(($ref->getLocalPropertyName() == $propRef->getLocalPropertyName())
				&& ($ref->getNamespace() == $propRef->getNamespace())
				&& ($ref->getAspectName() == $propRef->getAspectName())){
					$found = true;
					$result->add($property);
				}
			}
			if(!$found){
				// Check property reference and add empty property value
				$this->checkPropertyRef($propRef);
				$propValue = new BasicPropertyValue($propRef, NULL);
				$result->add($propValue);
			}
		}

		return $result;
	}

	public function getPropertyValueEPr($evidence, $propertyRef){
		$this->checkEvidence($evidence);
		$this->checkPropertyRef($propertyRef);
		
		$properties = $this->getPropertyValues($evidence)->getAll();
		foreach($properties as $property){
			$ref = $property->getPropertyRef();
			if(($ref->getLocalPropertyName() == $propertyRef->getLocalPropertyName())
			&& ($ref->getNamespace() == $propertyRef->getNamespace())
			&& ($ref->getAspectName() == $propertyRef->getAspectName())){
				return $property;
			}
		}

		// No property value found, return an empty value.
		$propValue = new BasicPropertyValue($propertyRef, NULL);
		return $propValue;
	}

	public function getPropertyValueEPn($evidence, $propertyName){
		$this->checkEvidence($evidence);
		$this->checkPropertyName($propertyName);
		
		$aspectName = $this->getDefaultAspectName(
			$propertyName->getLocalPropertyName(),
			$propertyName->getNamespace());
		$properties = $this->getPropertyValues($evidence)->getAll();
		foreach($properties as $property){
			$ref = $property->getPropertyRef();
			if(($ref->getLocalPropertyName() == $propertyName->getLocalPropertyName())
			&& ($ref->getNamespace() == $propertyName->getNamespace())
			&& ($ref->getAspectName() == $aspectName)){
				return $property;
			}
		}

		// No property value found, return an empty value.
		$propRef = new BasicPropertyRef($propertyName, $aspectName);
		$propValue = new BasicPropertyValue($propRef, NULL);
		return $propValue;
	}

	public function getPropertyValueES($evidence, $localPropertyName){
		$this->checkEvidence($evidence);
		$this->checkPropertyWithoutAspect($localPropertyName, $this->defaultVocabulary);

		$aspectName = $this->getDefaultAspectName(
			$localPropertyName, $this->defaultVocabulary);
			
		$properties = $this->getPropertyValues($evidence)->getAll();
		foreach($properties as $property){
			$ref = $property->getPropertyRef();
			if(($ref->getLocalPropertyName() == $localPropertyName)
			&& ($ref->getNamespace() == $this->defaultVocabulary)
			&& ($ref->getAspectName() == $aspectName)){
				return $property;
			}
		}

		// No property value found, return an empty value.
		$propName = new BasicPropertyName($this->defaultVocabulary, $localPropertyName);
		$propRef = new BasicPropertyRef($propName, $aspectName);
		$propValue = new BasicPropertyValue($propRef, NULL);
		return $propValue;
	}

	public function getPropertyValueESSS(
	$evidence, $localPropertyName, $localAspectName, $vocabularyIRI){
		$this->checkEvidence($evidence);
		$this->checkProperty($localPropertyName, $localAspectName, $vocabularyIRI);

		$properties = $this->getPropertyValues($evidence)->getAll();
		foreach($properties as $property){
			$ref = $property->getPropertyRef();
			if(($ref->getLocalPropertyName() == $localPropertyName)
			&& ($ref->getNamespace() == $vocabularyIRI)
			&& ($ref->getAspectName() == $localAspectName)){
				return $property;
			}
		}

		// No property value found, return an empty value.
		$propName = new BasicPropertyName($vocabularyIRI, $localPropertyName);
		$propRef = new BasicPropertyRef($propName, $localAspectName);
		$propValue = new BasicPropertyValue($propRef, NULL);
		return $propValue;
	}

	public function listPropertyRefs(){
		$res = array();
		foreach($this->supportedProperties as $vocab=>$props){
			foreach($props as $name=>$desc){
				$aspects = $desc['aspects'];
				foreach($aspects as $aspect){
					$propName = new BasicPropertyName($vocab, $name);
					$propRef = new BasicPropertyRef($propName, $aspect);
					$res[] = $propRef;
				}
			}
		}
		return $res;
	}

	/**
	 * Initializes the implementation.
	 *
	 * The method is called automatically when {@link ServiceFactory::newService()}
	 * is used to create an instance of the underlying concrete subclass.
	 * 
	 * The method should not need to be overridden in subclasses. It calls the
	 * abstract {@link initializeProperties()} method to initialize the list
	 * of supported properties and implementation-specific settings.
	 *
	 * @param string $defaultVocabularyIRI the IRI of the default vocabulary.
	 * @param array(string=>string) $props See above.
	 * @exception NameException the given vocabulary is not supported.
	 * @exception InitializationException there was a problem during initialization.
	 */
	public function initialize($defaultVocabularyIRI, $props){
		// Initializes the list of supported properties
		// and implementation-specific settings.
		$this->initializeProperties($props);
		
		$this->checkNamespace($defaultVocabularyIRI);
		$this->defaultVocabulary = $defaultVocabularyIRI;
	}

	/**
	 * Ensures that the given property reference is valid and supported by
	 * this implementation. Exceptions are thrown whenever something wrong
	 * is encountered.
	 *
	 * @param PropertyRef $propertyRef The property reference to check.
	 * @return void No return code, exceptions are thrown when argument is invalid.
	 * @exception SystemException The argument is not a valid PropertyRef.
	 * @exception NameException The property is not supported.
	 */
	protected function checkPropertyRef($propertyRef){
		if(!isset($propertyRef)){
			throw new SystemException(
				'The property reference cannot be null.',
				SystemException::$ILLEGAL_ARGUMENT);
		}
		if(!($propertyRef instanceof PropertyRef)){
			throw new SystemException(
				'The property name does not implement the PropertyRef interface.',
				SystemException::$ILLEGAL_ARGUMENT);
		}

		$this->checkProperty(
			$propertyRef->getLocalPropertyName(),
			$propertyRef->getAspectName(),
			$propertyRef->getNamespace());
	}

	/**
	 * Ensures that the given property name is valid and supported by
	 * this implementation. Exceptions are thrown whenever something wrong
	 * is encountered.
	 *
	 * @param PropertyName $propertyName The property to check.
	 * @return void No return code, exceptions are thrown when argument is invalid.
	 * @exception SystemException The argument is not a valid PropertyName.
	 * @exception NameException The property is not supported.
	 */
	protected function checkPropertyName($propertyName){
		if(!isset($propertyName)){
			throw new SystemException(
				'The property name cannot be null.',
				SystemException::$ILLEGAL_ARGUMENT);
		}
		if(!($propertyName instanceof PropertyName)){
			throw new SystemException(
				'The property name does not implement the PropertyName interface.',
				SystemException::$ILLEGAL_ARGUMENT);
		}

		$this->checkPropertyWithoutAspect(
			$propertyName->getLocalPropertyName(),
			$propertyName->getNamespace());
	}

	/**
	 * Ensures that the given aspect name is valid and supported by this
	 * implementation. Exceptions are thrown whenever something wrong is
	 * encountered.
	 *
	 * NB: This method does not attempt to validate the aspect name against
	 * a property name.
	 *
	 * @param string $localAspectName Aspect name to check.
	 * @return void No return code, exceptions are thrown when argument is invalid.
	 * @exception SystemException The argument is null.
	 * @exception NameException The aspect is not supported.
	 */
	protected function checkAspectName($localAspectName){
		if(!isset($localAspectName)){
			throw new SystemException(
				'The aspect name cannot be null.',
				SystemException::$ILLEGAL_ARGUMENT);
		}
		
		$found = false;
		foreach($this->supportedProperties as $vocab=>$props){
			foreach($props as $name=>$desc){
				$aspects = $desc['aspects'];
				if(in_array($localAspectName, $aspects)){
					$found = true;
					break;
				}
			}
		}
		if(!$found){
			throw new NameException(
				'The aspect name is not supported.',
				NameException::$ASPECT_NOT_RECOGNIZED);
		}
	}

	/**
	 * Ensures that the given vocabulary IRI is valid and supported by
	 * this implementation. Exceptions are thrown whenever something wrong
	 * is encountered.
	 *
	 * @param string $vocabularyIRI The IRI of the vocabulary to check.
	 * @return void No return code, exceptions are thrown when argument is invalid.
	 * @exception SystemException The argument is null.
	 * @exception NameException The vocabulary is not supported.
	 */
	protected function checkNamespace($vocabularyIRI){
		if(!isset($vocabularyIRI)){
			throw new SystemException(
				'The vocabulary IRI cannot be null.',
				SystemException::$ILLEGAL_ARGUMENT);
		}
		if(!array_key_exists($vocabularyIRI, $this->supportedProperties)
			|| !isset($this->supportedProperties[$vocabularyIRI])){
				throw new NameException(
				'The vocabulary is not supported.',
				NameException::$VOCABULARY_NOT_RECOGNIZED);
		}
		/* Code replaced because we don't know weather the key
		 * exists or not in the array.
		$props = $this->supportedProperties[$vocabularyIRI];
		if(!isset($props)){
			throw new NameException(
				'The vocabulary is not supported.',
				NameException::$VOCABULARY_NOT_RECOGNIZED);
		}
		*/
	}

	/**
	 * Ensures that the given evidence is valid and supported by this
	 * implementation. Exceptions are thrown whenever something wrong
	 * is encountered.
	 *
	 * @param Evidence $evidence The evidence to check.
	 * @return void No return code, exceptions are thrown when argument is invalid.
	 * @exception SystemException The argument is not a valid Evidence.
	 */
	protected function checkEvidence($evidence){
		if(!isset($evidence)){
			throw new SystemException(
				'The evidence cannot be null.',
				SystemException::$ILLEGAL_ARGUMENT);
		}
		if(!($evidence instanceof Evidence)){
			throw new SystemException(
				'The evidence does not implement the Evidence interface.',
				SystemException::$ILLEGAL_ARGUMENT);
		}
	}

	/**
	 * Ensures that the given property is valid and supported by this
	 * implementation. Exceptions are thrown whenever something wrong
	 * is encountered.
	 *
	 * @param string $localPropertyName The local property name (no namespace).
	 * @param string $localAspectName The local aspect name the property applies to.
	 * @param string $vocabularyIRI The namespace the property and the aspect belong to.
	 * @return void No return code, exceptions are thrown when argument is invalid.
	 * @exception SystemException One of the arguments is null.
	 * @exception NameException The final property is not supported, the code
	 * 		identifies the first non supported part of the property.
	 */
	protected function checkProperty($localPropertyName, $localAspectName, $vocabularyIRI){
		$this->checkNamespace($vocabularyIRI);
		if(!isset($localAspectName)){
			throw new SystemException(
				'The aspect name cannot be null.',
				SystemException::$ILLEGAL_ARGUMENT);
		}
		if(!isset($localPropertyName)){
			throw new SystemException(
				'The local property name cannot be null.',
				SystemException::$ILLEGAL_ARGUMENT);
		}

		$props = $this->supportedProperties[$vocabularyIRI];
		// $props is set, otherwise checkNamespace would have thrown an exception.
		
		if(!array_key_exists($localPropertyName, $props)
				|| !isset($props[$localPropertyName])){
			throw new NameException(
				'Unknown property name.',
				NameException::$PROPERTY_NOT_RECOGNIZED);
		}
		
		$prop = $props[$localPropertyName];
		
		if(!in_array($localAspectName, $prop['aspects'])){
			throw new NameException(
				'The aspect name is not supported.',
				NameException::$ASPECT_NOT_RECOGNIZED);
		}
	}

	/**
	 * Ensures that the given property is valid and supported by this
	 * implementation. Exceptions are thrown whenever something wrong
	 * is encountered.
	 *
	 * @param string $localPropertyName The local property name (no namespace).
	 * @param string $vocabularyIRI The namespace the property and the aspect belong to.
	 * @return void No return code, exceptions are thrown when argument is invalid.
	 * @exception SystemException One of the arguments is null.
	 * @exception NameException The final property is not supported, the code
	 * 		identifies the first non supported part of the property.
	 */
	protected function checkPropertyWithoutAspect($localPropertyName, $vocabularyIRI){
		$this->checkNamespace($vocabularyIRI);
		if(!isset($localPropertyName)){
			throw new SystemException(
				'The local property name cannot be null.',
				SystemException::$ILLEGAL_ARGUMENT);
		}
		
		$props = $this->supportedProperties[$vocabularyIRI];
		if (!array_key_exists($localPropertyName, $props)){
			throw new NameException(
				'Unknown property name.',
				NameException::$PROPERTY_NOT_RECOGNIZED);
		}
	}

	/**
	 * Returns the default aspect name associated with the property.
	 *
	 * @param string $localPropertyName The local property name.
	 * @param string $vocabularyIRI The vocabulary the property belongs to.
	 * @return string The default aspect name of the property when found.
	 */
	protected function getDefaultAspectName($localPropertyName, $vocabularyIRI){
		$props = $this->supportedProperties[$vocabularyIRI];
		$prop = $props[$localPropertyName];
		$aspects = $prop['aspects'];
		
		return $aspects[0];
	}
}
?>