Procházet zdrojové kódy

REST services: alpha2. It is now extensible (implement iRestServiceProvider). Still lacks two verbs: apply_stimulus and delete.

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@2592 a333f486-631f-4898-b8df-5754b55c2be0
romainq před 12 roky
rodič
revize
ad499b0237

+ 422 - 0
application/applicationextension.inc.php

@@ -526,3 +526,425 @@ interface iPageUIExtension
 	public function GetBannerHtml(iTopWebPage $oPage);
 }
 
+/**
+ * Implement this interface to add new operations to the REST/JSON web service
+ *  
+ * @package     Extensibility
+ * @api
+ * @since 2.0.1  
+ */
+interface iRestServiceProvider
+{
+	/**
+	 * Enumerate services delivered by this class
+	 * @param string $sVersion The version (e.g. 1.0) supported by the services
+	 * @return array An array of hash 'verb' => verb, 'description' => description
+	 */
+	public function ListOperations($sVersion);
+	/**
+	 * Enumerate services delivered by this class
+	 * @param string $sVersion The version (e.g. 1.0) supported by the services
+	 * @return RestResult The standardized result structure (at least a message)
+	 * @throws Exception in case of internal failure.	 
+	 */
+	public function ExecOperation($sVersion, $sVerb, $aParams);
+}
+
+/**
+ * Minimal REST response structure. Derive this structure to add response data and error codes.
+ *
+ * @package     Extensibility
+ * @api
+ * @since 2.0.1  
+ */
+class RestResult
+{
+	/**
+	 * Result: no issue has been encountered
+	 */
+	const OK = 0;
+	/**
+	 * Result: missing/wrong credentials or the user does not have enough rights to perform the requested operation 
+	 */
+	const UNAUTHORIZED = 1;
+	/**
+	 * Result: the parameter 'version' is missing
+	 */
+	const MISSING_VERSION = 2;
+	/**
+	 * Result: the parameter 'json_data' is missing
+	 */
+	const MISSING_JSON = 3;
+	/**
+	 * Result: the input structure is not a valid JSON string
+	 */
+	const INVALID_JSON = 4;
+	/**
+	 * Result: no operation is available for the specified version
+	 */
+	const UNSUPPORTED_VERSION = 10;
+	/**
+	 * Result: the requested operation is not valid for the specified version
+	 */
+	const UNKNOWN_OPERATION = 11;
+	/**
+	 * Result: the operation could not be performed, see the message for troubleshooting
+	 */
+	const INTERNAL_ERROR = 100;
+
+	/**
+	 * Default constructor - ok!
+	 * 	 
+	 * @param DBObject $oObject The object being reported
+	 * @param string $sAttCode The attribute code (must be valid)
+	 * @return string A scalar representation of the value
+	 */
+	public function __construct()
+	{
+		$this->code = RestResult::OK;
+	}
+
+	public $code;
+	public $message;
+}
+
+/**
+ * Helpers for implementing REST services
+ *
+ * @package     Extensibility
+ * @api
+ */
+class RestUtils
+{
+	/**
+	 * Registering tracking information. Any further object modification be associated with the given comment, when the modification gets recorded into the DB
+	 * 	 
+	 * @param StdClass $oData Structured input data. Must contain 'comment'.
+	 * @return void
+	 * @throws Exception
+	 * @api
+	 */
+	public static function InitTrackingComment($oData)
+	{
+		$sComment = self::GetMandatoryParam($oData, 'comment');
+		CMDBObject::SetTrackInfo($sComment);
+	}
+
+	/**
+	 * Read a mandatory parameter from  from a Rest/Json structure.
+	 * 	 
+	 * @param StdClass $oData Structured input data. Must contain the entry defined by sParamName.
+	 * @param string $sParamName Name of the parameter to fetch from the input data
+	 * @return void
+	 * @throws Exception If the parameter is missing
+	 * @api
+	 */
+	public static function GetMandatoryParam($oData, $sParamName)
+	{
+		if (isset($oData->$sParamName))
+		{
+			return $oData->$sParamName;
+		}
+		else
+		{
+			throw new Exception("Missing parameter '$sParamName'");
+		}
+	}
+
+
+	/**
+	 * Read an optional parameter from  from a Rest/Json structure.
+	 * 	 
+	 * @param StdClass $oData Structured input data.
+	 * @param string $sParamName Name of the parameter to fetch from the input data
+	 * @param mixed $default Default value if the parameter is not found in the input data
+	 * @return void
+	 * @throws Exception
+	 * @api
+	 */
+	public static function GetOptionalParam($oData, $sParamName, $default)
+	{
+		if (isset($oData->$sParamName))
+		{
+			return $oData->$sParamName;
+		}
+		else
+		{
+			return $default;
+		}
+	}
+
+
+	/**
+	 * Read a class  from a Rest/Json structure.
+	 *
+	 * @param StdClass $oData Structured input data. Must contain the entry defined by sParamName.
+	 * @param string $sParamName Name of the parameter to fetch from the input data
+	 * @return void
+	 * @throws Exception If the parameter is missing or the class is unknown
+	 * @api
+	 */
+	public static function GetClass($oData, $sParamName)
+	{
+		$sClass = self::GetMandatoryParam($oData, $sParamName);
+		if (!MetaModel::IsValidClass($sClass))
+		{
+			throw new Exception("$sParamName: '$sClass' is not a valid class'");
+		}
+		return $sClass;
+	}
+
+
+	/**
+	 * Read a list of attribute codes from a Rest/Json structure.
+	 * 	 
+	 * @param string $sClass Name of the class
+	 * @param StdClass $oData Structured input data.
+	 * @param string $sParamName Name of the parameter to fetch from the input data
+	 * @return void
+	 * @throws Exception
+	 * @api
+	 */
+	public static function GetFieldList($sClass, $oData, $sParamName)
+	{
+		$sFields = self::GetOptionalParam($oData, $sParamName, '*');
+		$aShowFields = array();
+		if ($sFields == '*')
+		{
+			foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
+			{
+				$aShowFields[] = $sAttCode;
+			}
+		}
+		else
+		{
+			foreach(explode(',', $sFields) as $sAttCode)
+			{
+				$sAttCode = trim($sAttCode);
+				if (($sAttCode != 'id') && (!MetaModel::IsValidAttCode($sClass, $sAttCode)))
+				{
+					throw new Exception("$sParamName: invalid attribute code '$sAttCode'");
+				}
+				$aShowFields[] = $sAttCode;
+			}
+		}
+		return $aShowFields;
+	}
+
+	/**
+	 * Read and interpret object search criteria from a Rest/Json structure
+	 * 	  	 
+	 * @param string $sClass Name of the class
+	 * @param StdClass $oCriteria Hash of attribute code => value (can be a substructure or a scalar, depending on the nature of the attriute)
+	 * @return object The object found
+	 * @throws Exception If the input structure is not valid or it could not find exactly one object
+	 */
+	protected static function FindObjectFromCriteria($sClass, $oCriteria)
+	{
+		$aCriteriaReport = array();
+		if (isset($oCriteria->finalclass))
+		{
+			$sClass = $oCriteria->finalclass;
+			if (!MetaModel::IsValidClass($sClass))
+			{
+				throw new Exception("finalclass: Unknown class '$sClass'");
+			}
+		}
+		$oSearch = new DBObjectSearch($sClass);
+		foreach ($oCriteria as $sAttCode => $value)
+		{
+			$realValue = self::MakeValue($sClass, $sAttCode, $value);
+			$oSearch->AddCondition($sAttCode, $realValue);
+			$aCriteriaReport[] = "$sAttCode: $value ($realValue)";
+		}
+		$oSet = new DBObjectSet($oSearch);
+		$iCount = $oSet->Count();
+		if ($iCount == 0)
+		{
+			throw new Exception("No item found with criteria: ".implode(', ', $aCriteriaReport));
+		}
+		elseif ($iCount > 1)
+		{
+			throw new Exception("Several items found ($iCount) with criteria: ".implode(', ', $aCriteriaReport));
+		}
+		$res = $oSet->Fetch();
+		return $res;
+	}
+
+
+	/**
+	 * Find an object from a polymorph search specification (Rest/Json)
+	 * 	 
+	 * @param string $sClass Name of the class
+	 * @param mixed $key Either search criteria (substructure), or an object or an OQL string.
+	 * @return DBObject The object found
+	 * @throws Exception If the input structure is not valid or it could not find exactly one object
+	 * @api
+	 */
+	public static function FindObjectFromKey($sClass, $key)
+	{
+		if (is_object($key))
+		{
+			$res = self::FindObjectFromCriteria($sClass, $key);
+		}
+		elseif (is_numeric($key))
+		{
+			$res = MetaModel::GetObject($sClass, $key);
+		}
+		elseif (is_string($key))
+		{
+			// OQL
+			$oSearch = DBObjectSearch::FromOQL($key);
+			$oSet = new DBObjectSet($oSearch);
+			$iCount = $oSet->Count();
+			if ($iCount == 0)
+			{
+				throw new Exception("No item found for query: $key");
+			}
+			elseif ($iCount > 1)
+			{
+				throw new Exception("Several items found ($iCount) for query: $key");
+			}
+			$res = $oSet->Fetch();
+		}
+		else
+		{
+			throw new Exception("Wrong format for key");
+		}
+		return $res;
+	}
+
+	/**
+	 * Search objects from a polymorph search specification (Rest/Json)
+	 * 	 
+	 * @param string $sClass Name of the class
+	 * @param mixed $key Either search criteria (substructure), or an object or an OQL string.
+	 * @return DBObjectSet The search result set
+	 * @throws Exception If the input structure is not valid
+	 */
+	public static function GetObjectSetFromKey($sClass, $key)
+	{
+		if (is_object($key))
+		{
+			if (isset($oCriteria->finalclass))
+			{
+				$sClass = $oCriteria->finalclass;
+				if (!MetaModel::IsValidClass($sClass))
+				{
+					throw new Exception("finalclass: Unknown class '$sClass'");
+				}
+			}
+		
+			$oSearch = new DBObjectSearch($sClass);
+			foreach ($key as $sAttCode => $value)
+			{
+				$realValue = self::MakeValue($sClass, $sAttCode, $value);
+				$oSearch->AddCondition($sAttCode, $realValue);
+			}
+		}
+		elseif (is_numeric($key))
+		{
+			$oSearch = new DBObjectSearch($sClass);
+			$oSearch->AddCondition('id', $key);
+		}
+		elseif (is_string($key))
+		{
+			// OQL
+			$oSearch = DBObjectSearch::FromOQL($key);
+			$oObjectSet = new DBObjectSet($oSearch);
+		}
+		else
+		{
+			throw new Exception("Wrong format for key");
+		}
+		$oObjectSet = new DBObjectSet($oSearch);
+		return $oObjectSet;
+	}
+
+	/**
+	 * Interpret the Rest/Json value and get a valid attribute value
+	 * 	 
+	 * @param string $sClass Name of the class
+	 * @param string $sAttCode Attribute code
+	 * @param mixed $value Depending on the type of attribute (a scalar, or search criteria, or list of related objects...)
+	 * @return mixed The value that can be used with DBObject::Set()
+	 * @throws Exception If the specification of the value is not valid.
+	 * @api
+	 */
+	public static function MakeValue($sClass, $sAttCode, $value)
+	{
+		try
+		{
+			if (!MetaModel::IsValidAttCode($sClass, $sAttCode))
+			{
+				throw new Exception("Unknown attribute");
+			}
+			$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
+			if ($oAttDef instanceof AttributeExternalKey)
+			{
+				$oExtKeyObject = self::FindObjectFromKey($oAttDef->GetTargetClass(), $value);
+				$value = $oExtKeyObject->GetKey();
+			}
+			elseif ($oAttDef instanceof AttributeLinkedSet)
+			{
+				if (!is_array($value))
+				{
+					throw new Exception("A link set must be defined by an array of objects");
+				}
+				$sLnkClass = $oAttDef->GetLinkedClass();
+				$aLinks = array();
+				foreach($value as $oValues)
+				{
+					$oLnk = self::MakeObjectFromFields($sLnkClass, $oValues);
+					$aLinks[] = $oLnk;
+				}
+				$value = DBObjectSet::FromArray($sLnkClass, $aLinks);
+			}
+		}
+		catch (Exception $e)
+		{
+			throw new Exception("$sAttCode: ".$e->getMessage(), $e->getCode());
+		}
+		return $value;
+	}
+
+	/**
+	 * Interpret a Rest/Json structure that defines attribute values, and build an object
+	 * 	 
+	 * @param string $sClass Name of the class
+	 * @param array $aFields A hash of attribute code => value specification.
+	 * @return DBObject The newly created object
+	 * @throws Exception If the specification of the values is not valid
+	 * @api
+	 */
+	public static function MakeObjectFromFields($sClass, $aFields)
+	{
+		$oObject = MetaModel::NewObject($sClass);
+		foreach ($aFields as $sAttCode => $value)
+		{
+			$realValue = self::MakeValue($sClass, $sAttCode, $value);
+			$oObject->Set($sAttCode, $realValue);
+		}
+		return $oObject;
+	}
+
+	/**
+	 * Interpret a Rest/Json structure that defines attribute values, and update the given object
+	 * 	 
+	 * @param DBObject $oObject The object being modified
+	 * @param array $aFields A hash of attribute code => value specification.
+	 * @return DBObject The object modified
+	 * @throws Exception If the specification of the values is not valid
+	 * @api
+	 */
+	public static function UpdateObjectFromFields($oObject, $aFields)
+	{
+		$sClass = get_class($oObject);
+		foreach ($aFields as $sAttCode => $value)
+		{
+			$realValue = self::MakeValue($sClass, $sAttCode, $value);
+			$oObject->Set($sAttCode, $realValue);
+		}
+		return $oObject;
+	}
+}

+ 253 - 0
core/restservices.class.inc.php

@@ -0,0 +1,253 @@
+<?php
+// Copyright (C) 2013 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify	
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
+/**
+ * REST/json services
+ * 
+ * Definition of common structures + the very minimum service provider (manage objects)
+ *
+ * @package     REST Services
+ * @copyright   Copyright (C) 2013 Combodo SARL
+ * @license     http://opensource.org/licenses/AGPL-3.0
+ * @api
+ */
+
+/**
+ * Element of the response formed by RestResultWithObjects
+ *
+ * @package     REST Services
+ */
+class ObjectResult
+{
+	public $code;
+	public $message;
+	public $fields;
+	
+	/**
+	 * Default constructor
+	 */
+	public function __construct()
+	{
+		$this->code = RestResult::OK;
+		$this->message = '';
+		$this->fields = array();
+	}
+
+	/**
+	 * Helper to make an output value for a given attribute
+	 * 	 
+	 * @param DBObject $oObject The object being reported
+	 * @param string $sAttCode The attribute code (must be valid)
+	 * @return string A scalar representation of the value
+	 */
+	protected function MakeResultValue(DBObject $oObject, $sAttCode)
+	{
+		if ($sAttCode == 'id')
+		{
+			$value = $oObject->GetKey();
+		}
+		else
+		{
+			$sClass = get_class($oObject);
+			$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
+			if ($oAttDef instanceof AttributeLinkedSet)
+			{
+				$value = array();
+
+				// Make the list of required attributes
+				// - Skip attributes pointing to the current object (redundant data)
+				// - Skip link sets refering to the current data (infinite recursion!)
+				$aRelevantAttributes = array();
+				$sLnkClass = $oAttDef->GetLinkedClass();
+				foreach (MetaModel::ListAttributeDefs($sLnkClass) as $sLnkAttCode => $oLnkAttDef)
+				{
+					// Skip any attribute of the link that points to the current object
+					//
+					if ($sLnkAttCode == $oAttDef->GetExtKeyToMe()) continue;
+					if (method_exists($oLnkAttDef, 'GetKeyAttCode'))
+					{
+						if ($oLnkAttDef->GetKeyAttCode() ==$oAttDef->GetExtKeyToMe()) continue;
+					}
+
+					$aRelevantAttributes[] = $sLnkAttCode;
+				}
+
+				// Iterate on the set and build an array of array of attcode=>value
+				$oSet = $oObject->Get($sAttCode);
+				while ($oLnk = $oSet->Fetch())
+				{
+					$aLnkValues = array();
+					foreach ($aRelevantAttributes as $sLnkAttCode)
+					{
+						$aLnkValues[$sLnkAttCode] = $this->MakeResultValue($oLnk, $sLnkAttCode);
+					}
+					$value[] = $aLnkValues;
+				}
+			}
+			else
+			{
+				$value = $oObject->GetEditValue($sAttCode);
+			}
+		}
+		return $value;
+	}
+
+	/**
+	 * Report the value for the given object attribute
+	 * 	 
+	 * @param DBObject $oObject The object being reported
+	 * @param string $sAttCode The attribute code (must be valid)
+	 * @return void
+	 */
+	public function AddField(DBObject $oObject, $sAttCode)
+	{
+		$this->fields[$sAttCode] = $this->MakeResultValue($oObject, $sAttCode);
+	}
+}
+
+
+
+/**
+ * REST response for services managing objects. Derive this structure to add information and/or constants
+ *
+ * @package     Extensibility
+ * @package     REST Services
+ * @api
+ */
+class RestResultWithObjects extends RestResult
+{
+	public $objects;
+
+	/**
+	 * Report the given object
+	 * 	 
+	 * @param int An error code (RestResult::OK is no issue has been found)
+	 * @param string $sMessage Description of the error if any, an empty string otherwise
+	 * @param DBObject $oObject The object being reported
+	 * @param array $aFields An array of attribute codes. List of the attributes to be reported.
+	 * @return void
+	 */
+	public function AddObject($iCode, $sMessage, $oObject = null, $aFields = null)
+	{
+		$oObjRes = new ObjectResult();
+		$oObjRes->code = $iCode;
+		$oObjRes->message = $sMessage;
+
+		if ($oObject)
+		{
+			foreach ($aFields as $sAttCode)
+			{
+				$oObjRes->AddField($oObject, $sAttCode);
+			}
+		}
+
+		$this->objects[] = $oObjRes;
+	}
+}
+
+
+/**
+ * Implementation of core REST services (create/get/update... objects)
+ *
+ * @package     Core
+ */
+class CoreServices implements iRestServiceProvider
+{
+	/**
+	 * Enumerate services delivered by this class
+	 * 	 
+	 * @param string $sVersion The version (e.g. 1.0) supported by the services
+	 * @return array An array of hash 'verb' => verb, 'description' => description
+	 */
+	public function ListOperations($sVersion)
+	{
+		$aOps = array();
+		if ($sVersion == '1.0')
+		{
+			$aOps[] = array(
+				'verb' => 'core/create',
+				'description' => 'Create an object'
+			);
+			$aOps[] = array(
+				'verb' => 'core/update',
+				'description' => 'Update an object'
+			);
+			$aOps[] = array(
+				'verb' => 'core/get',
+				'description' => 'Search for objects'
+			);
+		}
+		return $aOps;
+	}
+
+	/**
+	 * Enumerate services delivered by this class
+	 * @param string $sVersion The version (e.g. 1.0) supported by the services
+	 * @return RestResult The standardized result structure (at least a message)
+	 * @throws Exception in case of internal failure.	 
+	 */
+	public function ExecOperation($sVersion, $sVerb, $aParams)
+	{
+		$oResult = new RestResultWithObjects();
+		switch ($sVerb)
+		{
+		case 'core/create':
+			RestUtils::InitTrackingComment($aParams);
+			$sClass = RestUtils::GetClass($aParams, 'class');
+			$aFields = RestUtils::GetMandatoryParam($aParams, 'fields');
+			$aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields');
+	
+			$oObject = RestUtils::MakeObjectFromFields($sClass, $aFields);
+			$oObject->DBInsert();
+	
+			$oResult->AddObject(0, 'created', $oObject, $aShowFields);
+			break;
+	
+		case 'core/update':
+			RestUtils::InitTrackingComment($aParams);
+			$sClass = RestUtils::GetClass($aParams, 'class');
+			$key = RestUtils::GetMandatoryParam($aParams, 'key');
+			$aFields = RestUtils::GetMandatoryParam($aParams, 'fields');
+			$aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields');
+	
+			$oObject = RestUtils::FindObjectFromKey($sClass, $key);
+			RestUtils::UpdateObjectFromFields($oObject, $aFields);
+			$oObject->DBUpdate();
+	
+			$oResult->AddObject(0, 'updated', $oObject, $aShowFields);
+			break;
+	
+		case 'core/get':
+			$sClass = RestUtils::GetClass($aParams, 'class');
+			$key = RestUtils::GetMandatoryParam($aParams, 'key');
+			$aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields');
+	
+			$oObjectSet = RestUtils::GetObjectSetFromKey($sClass, $key);
+			while ($oObject = $oObjectSet->Fetch())
+			{
+				$oResult->AddObject(0, '', $oObject, $aShowFields);
+			}
+			$oResult->message = "Found: ".$oObjectSet->Count();
+			break;
+	
+		default:
+			// unknown operation: handled at a higher level
+		}
+		return $oResult;
+	}
+}

+ 11 - 8
webservices/itoprest.examples.php

@@ -92,10 +92,13 @@ function DoPostRequest_curl($sUrl, $aData)
 
 $aOperations = array(
 	array(
-		'operation' => 'object_create', // operation code
+		'operation' => 'list_operations', // operation code
+	),
+	array(
+		'operation' => 'core/create', // operation code
 		'comment' => 'Synchronization from blah...', // comment recorded in the change tracking log
 		'class' => 'UserRequest',
-		'results' => 'id, friendlyname', // list of fields to show in the results (* or a,b,c)
+		'output_fields' => 'id, friendlyname', // list of fields to show in the results (* or a,b,c)
 		// Values for the object to create
 		'fields' => array(
 			'org_id' => "SELECT Organization WHERE name = 'Demo'",
@@ -105,11 +108,11 @@ $aOperations = array(
 		),
 	),
 	array(
-		'operation' => 'object_update', // operation code
+		'operation' => 'core/update', // operation code
 		'comment' => 'Synchronization from blah...', // comment recorded in the change tracking log
 		'class' => 'UserRequest',
 		'key' => 'SELECT UserRequest WHERE id=1',
-		'results' => 'id, friendlyname, title', // list of fields to show in the results (* or a,b,c)
+		'output_fields' => 'id, friendlyname, title', // list of fields to show in the results (* or a,b,c)
 		// Values for the object to create
 		'fields' => array(
 			'title' => 'Issue #'.rand(0, 100),
@@ -122,15 +125,14 @@ $aOperations = array(
 		),
 	),
 	array(
-		'operation' => 'object_get', // operation code
+		'operation' => 'core/get', // operation code
 		'class' => 'UserRequest',
 		'key' => 'SELECT UserRequest',
-		'results' => 'id, friendlyname, title, contacts_list', // list of fields to show in the results (* or a,b,c)
+		'output_fields' => 'id, friendlyname, title, contacts_list', // list of fields to show in the results (* or a,b,c)
 	),
 );
 
-
-$sUrl = "http://localhost/rest-services/webservices/rest.php?version=0.9";
+$sUrl = "http://localhost/rest-services/webservices/rest.php?version=1.0";
 
 $aData = array();
 $aData['auth_user'] = 'admin';
@@ -148,6 +150,7 @@ foreach ($aOperations as $iOp => $aOperation)
 
 	$response = DoPostRequest($sUrl, $aData);
 	$aResults = json_decode($response);
+	$aResults = $response;
 	if ($aResults)
 	{
 		echo "--------------------------------------\n";

+ 85 - 384
webservices/rest.php

@@ -61,448 +61,149 @@
 if (!defined('__DIR__')) define('__DIR__', dirname(__FILE__));
 require_once(__DIR__.'/../approot.inc.php');
 require_once(APPROOT.'/application/application.inc.php');
-require_once(APPROOT.'/application/clipage.class.inc.php');
+require_once(APPROOT.'/application/ajaxwebpage.class.inc.php');
 require_once(APPROOT.'/application/startup.inc.php');
 
+require_once(APPROOT.'core/restservices.class.inc.php');
 
 
-class RestServices
+/**
+ * Result structure that is specific to the hardcoded verb 'list_operations'
+ */ 
+class RestResultListOperations extends RestResult
 {
-	public function InitTrackingComment($oData)
-	{
-		$sComment = $this->GetMandatoryParam($oData, 'comment');
-		CMDBObject::SetTrackInfo($sComment);
-	}
+	public $version;
+	public $operations;
 
-
-	public function GetMandatoryParam($oData, $sParamName)
+	public function AddOperation($sVerb, $sDescription, $sServiceProviderClass)
 	{
-		if (isset($oData->$sParamName))
-		{
-			return $oData->$sParamName;
-		}
-		else
-		{
-			throw new Exception("Missing parameter '$sParamName'");
-		}
+		$this->operations[] = array(
+			'verb' => $sVerb,
+			'description' => $sDescription,
+			'extension' => $sServiceProviderClass
+		);
 	}
+}
 
+////////////////////////////////////////////////////////////////////////////////
+//
+// Main
+//
+$oP = new ajax_page('rest');
 
-	public function GetOptionalParam($oData, $sParamName, $default)
+try
+{
+	utils::UseParamFile();
+
+	$sAuthUser = utils::ReadPostedParam('auth_user', null, 'raw_data');
+	$sAuthPwd = utils::ReadPostedParam('auth_pwd', null, 'raw_data');
+	if (UserRights::CheckCredentials($sAuthUser, $sAuthPwd))
 	{
-		if (isset($oData->$sParamName))
-		{
-			return $oData->$sParamName;
-		}
-		else
-		{
-			return $default;
-		}
+		UserRights::Login($sAuthUser); // Login & set the user's language
 	}
-
-
-	public function GetClass($oData, $sParamName)
+	else
 	{
-		$sClass = $this->GetMandatoryParam($oData, $sParamName);
-		if (!MetaModel::IsValidClass($sClass))
-		{
-			throw new Exception("$sParamName: '$sClass' is not a valid class'");
-		}
-		return $sClass;
+		throw new Exception("Invalid login '$sAuthUser'", RestResult::UNAUTHORIZED);
 	}
 
-
-	public function GetFieldList($sClass, $oData, $sParamName)
+	$sVersion = utils::ReadParam('version', null, false, 'raw_data');
+	if ($sVersion == null)
 	{
-		$sFields = $this->GetOptionalParam($oData, $sParamName, '*');
-		$aShowFields = array();
-		if ($sFields == '*')
-		{
-			foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
-			{
-				$aShowFields[] = $sAttCode;
-			}
-		}
-		else
-		{
-			foreach(explode(',', $sFields) as $sAttCode)
-			{
-				$sAttCode = trim($sAttCode);
-				if (($sAttCode != 'id') && (!MetaModel::IsValidAttCode($sClass, $sAttCode)))
-				{
-					throw new Exception("$sParamName: invalid attribute code '$sAttCode'");
-				}
-				$aShowFields[] = $sAttCode;
-			}
-		}
-		return $aShowFields;
+		throw new Exception("Missing parameter 'version' (e.g. '1.0')", RestResult::MISSING_VERSION);
 	}
-
-	protected function FindObjectFromCriteria($sClass, $oCriteria)
+	
+	$sJsonString = utils::ReadPostedParam('json_data', null, 'raw_data');
+	if ($sJsonString == null)
 	{
-		$aCriteriaReport = array();
-		if (isset($oCriteria->finalclass))
-		{
-			$sClass = $oCriteria->finalclass;
-			if (!MetaModel::IsValidClass($sClass))
-			{
-				throw new Exception("finalclass: Unknown class '$sClass'");
-			}
-		}
-		$oSearch = new DBObjectSearch($sClass);
-		foreach ($oCriteria as $sAttCode => $value)
-		{
-			$realValue = $this->MakeValue($sClass, $sAttCode, $value);
-			$oSearch->AddCondition($sAttCode, $realValue);
-			$aCriteriaReport[] = "$sAttCode: $value ($realValue)";
-		}
-		$oSet = new DBObjectSet($oSearch);
-		$iCount = $oSet->Count();
-		if ($iCount == 0)
-		{
-			throw new Exception("No item found for criteria: ".implode(', ', $aCriteriaReport));
-		}
-		elseif ($iCount > 1)
-		{
-			throw new Exception("Several items found ($iCount) for criteria: ".implode(', ', $aCriteriaReport));
-		}
-		$res = $oSet->Fetch();
-		return $res;
+		throw new Exception("Missing parameter 'json_data", RestResult::MISSING_JSON);
 	}
-
-
-	public function FindObjectFromKey($sClass, $key)
+	$aJsonData = json_decode($sJsonString);
+	if ($aJsonData == null)
 	{
-		if (is_object($key))
-		{
-			$res = $this->FindObjectFromCriteria($sClass, $key);
-		}
-		elseif (is_numeric($key))
-		{
-			$res = MetaModel::GetObject($sClass, $key);
-		}
-		elseif (is_string($key))
-		{
-			// OQL
-			$oSearch = DBObjectSearch::FromOQL($key);
-			$oSet = new DBObjectSet($oSearch);
-			$iCount = $oSet->Count();
-			if ($iCount == 0)
-			{
-				throw new Exception("No item found for query: $key");
-			}
-			elseif ($iCount > 1)
-			{
-				throw new Exception("Several items found ($iCount) for query: $key");
-			}
-			$res = $oSet->Fetch();
-		}
-		else
-		{
-			throw new Exception("Wrong format for key");
-		}
-		return $res;
+		throw new Exception("Parameter json_data is not a valid JSON structure", RestResult::INVALID_JSON);
 	}
 
 
-	public function GetObjectSetFromKey($sClass, $key)
+	$aProviders = array();
+	foreach(get_declared_classes() as $sPHPClass)
 	{
-		if (is_object($key))
+		$oRefClass = new ReflectionClass($sPHPClass);
+		if ($oRefClass->implementsInterface('iRestServiceProvider'))
 		{
-			if (isset($oCriteria->finalclass))
-			{
-				$sClass = $oCriteria->finalclass;
-				if (!MetaModel::IsValidClass($sClass))
-				{
-					throw new Exception("finalclass: Unknown class '$sClass'");
-				}
-			}
-		
-			$oSearch = new DBObjectSearch($sClass);
-			foreach ($key as $sAttCode => $value)
-			{
-				$realValue = $this->MakeValue($sClass, $sAttCode, $value);
-				$oSearch->AddCondition($sAttCode, $realValue);
-			}
+			$aProviders[] = new $sPHPClass;
 		}
-		elseif (is_numeric($key))
-		{
-			$oSearch = new DBObjectSearch($sClass);
-			$oSearch->AddCondition('id', $key);
-		}
-		elseif (is_string($key))
-		{
-			// OQL
-			$oSearch = DBObjectSearch::FromOQL($key);
-			$oObjectSet = new DBObjectSet($oSearch);
-		}
-		else
-		{
-			throw new Exception("Wrong format for key");
-		}
-		$oObjectSet = new DBObjectSet($oSearch);
-		return $oObjectSet;
 	}
 
-
-	protected function MakeValue($sClass, $sAttCode, $value)
+	$aOpToRestService = array(); // verb => $oRestServiceProvider
+	foreach ($aProviders as $oRestSP)
 	{
-		try
+		$aOperations = $oRestSP->ListOperations($sVersion);
+		foreach ($aOperations as $aOpData)
 		{
-			if (!MetaModel::IsValidAttCode($sClass, $sAttCode))
-			{
-				throw new Exception("Unknown attribute");
-			}
-			$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
-			if ($oAttDef instanceof AttributeExternalKey)
-			{
-				$oExtKeyObject = $this->FindObjectFromKey($oAttDef->GetTargetClass(), $value);
-				$value = $oExtKeyObject->GetKey();
-			}
-			elseif ($oAttDef instanceof AttributeLinkedSet)
-			{
-				if (!is_array($value))
-				{
-					throw new Exception("A link set must be defined by an array of objects");
-				}
-				$sLnkClass = $oAttDef->GetLinkedClass();
-				$aLinks = array();
-				foreach($value as $oValues)
-				{
-					$oLnk = $this->MakeObjectFromFields($sLnkClass, $oValues);
-					$aLinks[] = $oLnk;
-				}
-				$value = DBObjectSet::FromArray($sLnkClass, $aLinks);
-			}
+			$aOpToRestService[$aOpData['verb']] = array
+			(
+				'service_provider' => $oRestSP,
+				'description' => $aOpData['description'],
+			);
 		}
-		catch (Exception $e)
-		{
-			throw new Exception("$sAttCode: ".$e->getMessage());
-		}
-		return $value;
 	}
 
-
-	public function MakeObjectFromFields($sClass, $aFields)
+	if (count($aOpToRestService) == 0)
 	{
-		$oObject = MetaModel::NewObject($sClass);
-		foreach ($aFields as $sAttCode => $value)
-		{
-			$realValue = $this->MakeValue($sClass, $sAttCode, $value);
-			$oObject->Set($sAttCode, $realValue);
-		}
-		return $oObject;
+		throw new Exception("There is no service available for version '$sVersion'", RestResult::UNSUPPORTED_VERSION);
 	}
 
 
-	public function UpdateObjectFromFields($oObject, $aFields)
+	$sOperation = RestUtils::GetMandatoryParam($aJsonData, 'operation');
+	if ($sOperation == 'list_operations')
 	{
-		$sClass = get_class($oObject);
-		foreach ($aFields as $sAttCode => $value)
+		$oResult = new RestResultListOperations();
+		$oResult->message = "Operations: ".count($aOpToRestService);
+		$oResult->version = $sVersion;
+		foreach ($aOpToRestService as $sVerb => $aOpData)
 		{
-			$realValue = $this->MakeValue($sClass, $sAttCode, $value);
-			$oObject->Set($sAttCode, $realValue);
+			$oResult->AddOperation($sVerb, $aOpData['description'], get_class($aOpData['service_provider']));
 		}
-		return $oObject;
-	}
-}
-
-class FieldResult
-{
-	protected $value;
-	
-	public function __construct()
-	{
-	}
-
-	public function GetValue()
-	{
-	}
-}
-
-class ObjectResult
-{
-	public $code;
-	public $message;
-	public $fields;
-	
-	public function __construct()
-	{
-		$this->code = 0;
-		$this->message = '';
-		$this->fields = array();
 	}
-
-	protected function MakeResultValue($oObject, $sAttCode)
+	else
 	{
-		if ($sAttCode == 'id')
+		if (!array_key_exists($sOperation, $aOpToRestService))
 		{
-			$value = $oObject->GetKey();
-		}
-		else
-		{
-			$sClass = get_class($oObject);
-			$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
-			if ($oAttDef instanceof AttributeLinkedSet)
-			{
-				$value = array();
-
-				// Make the list of required attributes
-				// - Skip attributes pointing to the current object (redundant data)
-				// - Skip link sets refering to the current data (infinite recursion!)
-				$aRelevantAttributes = array();
-				$sLnkClass = $oAttDef->GetLinkedClass();
-				foreach (MetaModel::ListAttributeDefs($sLnkClass) as $sLnkAttCode => $oLnkAttDef)
-				{
-					// Skip any attribute of the link that points to the current object
-					//
-					if ($sLnkAttCode == $oAttDef->GetExtKeyToMe()) continue;
-					if (method_exists($oLnkAttDef, 'GetKeyAttCode'))
-					{
-						if ($oLnkAttDef->GetKeyAttCode() ==$oAttDef->GetExtKeyToMe()) continue;
-					}
-
-					$aRelevantAttributes[] = $sLnkAttCode;
-				}
-
-				// Iterate on the set and build an array of array of attcode=>value
-				$oSet = $oObject->Get($sAttCode);
-				while ($oLnk = $oSet->Fetch())
-				{
-					$aLnkValues = array();
-					foreach ($aRelevantAttributes as $sLnkAttCode)
-					{
-						$aLnkValues[$sLnkAttCode] = $this->MakeResultValue($oLnk, $sLnkAttCode);
-					}
-					$value[] = $aLnkValues;
-				}
-			}
-			else
-			{
-				$value = $oObject->GetEditValue($sAttCode);
-			}
+			throw new Exception("Unknown verb '$sVersion'", RestResult::UNKNOWN_OPERATION);
 		}
-		return $value;
-	}
-
-	public function AddField($oObject, $sAttCode)
-	{
-		$this->fields[$sAttCode] = $this->MakeResultValue($oObject, $sAttCode);
+		$oRS = $aOpToRestService[$sOperation]['service_provider'];
+	
+		$oResult = $oRS->ExecOperation($sVersion, $sOperation, $aJsonData);
 	}
 }
-
-class RestResult
+catch(Exception $e)
 {
-	public function __construct()
+	$oResult = new RestResult();
+	if ($e->GetCode() == 0)
 	{
+		$oResult->code = RestResult::INTERNAL_ERROR;
 	}
-
-	public $code;
-	public $message;
-	public $objects;
-
-	public function AddObject($iCode, $sMessage, $oObject = null, $aFields = null)
+	else
 	{
-		$oObjRes = new ObjectResult();
-		$oObjRes->code = $iCode;
-		$oObjRes->message = $sMessage;
-
-		if ($oObject)
-		{
-			foreach ($aFields as $sAttCode)
-			{
-				$oObjRes->AddField($oObject, $sAttCode);
-			}
-		}
-
-		$this->objects[] = $oObjRes;
+		$oResult->code = $e->GetCode();
 	}
+	$oResult->message = "Error: ".$e->GetMessage();
 }
 
-
-
-////////////////////////////////////////////////////////////////////////////////
+// Output the results
 //
-// Main
-//
-$oP = new CLIPage("iTop - REST");
-$oResult = new RestResult();
+$oP->add_header('Access-Control-Allow-Origin: *');
 
-try
+$sCallback = utils::ReadParam('callback', null);
+if ($sCallback == null)
 {
-	utils::UseParamFile();
-
-	$sAuthUser = utils::ReadPostedParam('auth_user', null, 'raw_data');
-	$sAuthPwd = utils::ReadPostedParam('auth_pwd', null, 'raw_data');
-	if (UserRights::CheckCredentials($sAuthUser, $sAuthPwd))
-	{
-		UserRights::Login($sAuthUser); // Login & set the user's language
-	}
-	else
-	{
-		throw new Exception("Invalid login '$sAuthUser'");
-	}
-	
-	$aJsonData = json_decode(utils::ReadPostedParam('json_data', null, 'raw_data'));
-	if ($aJsonData == null)
-	{
-		throw new Exception('Parameter json_data is not a valid JSON structure');
-	}
-
-	$oRS = new RestServices();
-
-	$sOperation = $oRS->GetMandatoryParam($aJsonData, 'operation');
-	switch ($sOperation)
-	{
-	case 'object_create':
-		$oRS->InitTrackingComment($aJsonData);
-		$sClass = $oRS->GetClass($aJsonData, 'class');
-		$aFields = $oRS->GetMandatoryParam($aJsonData, 'fields');
-		$aShowFields = $oRS->GetFieldList($sClass, $aJsonData, 'results');
-
-		$oObject = $oRS->MakeObjectFromFields($sClass, $aFields);
-		$oObject->DBInsert();
-
-		$oResult->AddObject(0, 'created', $oObject, $aShowFields);
-		break;
-
-	case 'object_update':
-		$oRS->InitTrackingComment($aJsonData);
-		$sClass = $oRS->GetClass($aJsonData, 'class');
-		$key = $oRS->GetMandatoryParam($aJsonData, 'key');
-		$aFields = $oRS->GetMandatoryParam($aJsonData, 'fields');
-		$aShowFields = $oRS->GetFieldList($sClass, $aJsonData, 'results');
-
-		$oObject = $oRS->FindObjectFromKey($sClass, $key);
-		$oRS->UpdateObjectFromFields($oObject, $aFields);
-		$oObject->DBUpdate();
-
-		$oResult->AddObject(0, 'updated', $oObject, $aShowFields);
-		break;
-
-	case 'object_get':
-		$sClass = $oRS->GetClass($aJsonData, 'class');
-		$key = $oRS->GetMandatoryParam($aJsonData, 'key');
-		$aShowFields = $oRS->GetFieldList($sClass, $aJsonData, 'results');
-
-		$oObjectSet = $oRS->GetObjectSetFromKey($sClass, $key);
-		while ($oObject = $oObjectSet->Fetch())
-		{
-			$oResult->AddObject(0, '', $oObject, $aShowFields);
-		}
-		$oResult->message = "Found: ".$oObjectSet->Count();
-		break;
-
-	default:
-		throw new Exception("Uknown operation '$sOperation'");
-	}
+	$oP->SetContentType('application/json');
+	$oP->add(json_encode($oResult));
 }
-catch(Exception $e)
+else
 {
-	$oResult->code = 1234;
-	$oResult->message = "Error: ".$e->GetMessage();
+	$oP->SetContentType('application/javascript');
+	$oP->add($sCallback.'('.json_encode($oResult).')');
 }
-
-$oP->add(json_encode($oResult));
 $oP->Output();
 ?>