Quellcode durchsuchen

Internal: Implemented DBObject::ExecActions, enables scripting object preset/modifications

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@3991 a333f486-631f-4898-b8df-5754b55c2be0
romainq vor 9 Jahren
Ursprung
Commit
9d63033e48
2 geänderte Dateien mit 509 neuen und 0 gelöschten Zeilen
  1. 355 0
      core/dbobject.class.php
  2. 154 0
      test/testlist.inc.php

+ 355 - 0
core/dbobject.class.php

@@ -502,6 +502,11 @@ abstract class DBObject implements iDisplay
 
 	public function GetStrict($sAttCode)
 	{
+		if ($sAttCode == 'id')
+		{
+			return $this->m_iKey;
+		}
+
 		$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
 
 		if (!$oAttDef->LoadInObject())
@@ -3063,5 +3068,355 @@ abstract class DBObject implements iDisplay
 		}
 		return $sFingerprint;
 	}
+
+	/**
+	 * Execute a set of scripted actions onto the current object
+	 * See ExecAction for the syntax and features of the scripted actions
+	 *
+	 * @param $aActions array of statements (e.g. "set(name, Made after $source->name$)")
+	 * @param $aSourceObjects Array of Alias => Context objects (Convention: some statements require the 'source' element
+	 * @throws Exception
+	 */
+	public function ExecActions($aActions, $aSourceObjects)
+	{
+		foreach($aActions as $sAction)
+		{
+			try
+			{
+				if (preg_match('/^(\S*)\s*\((.*)\)$/ms', $sAction, $aMatches)) // multiline and newline matched by a dot
+				{
+					$sVerb = trim($aMatches[1]);
+					$sParams = $aMatches[2];
+
+					// the coma is the separator for the parameters
+					// comas can be escaped: \,
+					$sParams = str_replace(array("\\\\", "\\,"), array("__backslash__", "__coma__"), $sParams);
+					$sParams = trim($sParams);
+
+					if (strlen($sParams) == 0)
+					{
+						$aParams = array();
+					}
+					else
+					{
+						$aParams = explode(',', $sParams);
+						foreach ($aParams as &$sParam)
+						{
+							$sParam = str_replace(array("__backslash__", "__coma__"), array("\\", ","), $sParam);
+							$sParam = trim($sParam);
+						}
+					}
+					$this->ExecAction($sVerb, $aParams, $aSourceObjects);
+				}
+				else
+				{
+					throw new Exception("Invalid syntax");
+				}
+			}
+			catch(Exception $e)
+			{
+				throw new Exception('Action: '.$sAction.' - '.$e->getMessage());
+			}
+		}
+	}
+
+	/**
+	 * Helper to copy an attribute between two objects (in memory)
+	 * Originally designed for ExecAction()
+	 */
+	public function CopyAttribute($oSourceObject, $sSourceAttCode, $sDestAttCode)
+	{
+		if ($sSourceAttCode == 'id')
+		{
+			$oSourceAttDef = null;
+		}
+		else
+		{
+			if (!MetaModel::IsValidAttCode(get_class($this), $sDestAttCode))
+			{
+				throw new Exception("Unknown attribute ".get_class($this)."::".$sDestAttCode);
+			}
+			if (!MetaModel::IsValidAttCode(get_class($oSourceObject), $sSourceAttCode))
+			{
+				throw new Exception("Unknown attribute ".get_class($oSourceObject)."::".$sSourceAttCode);
+			}
+
+			$oSourceAttDef = MetaModel::GetAttributeDef(get_class($oSourceObject), $sSourceAttCode);
+		}
+		if (is_object($oSourceAttDef) && $oSourceAttDef->IsLinkSet())
+		{
+			// The copy requires that we create a new object set (the semantic of DBObject::Set is unclear about link sets)
+			$oDestSet = DBObjectSet::FromScratch($oSourceAttDef->GetLinkedClass());
+			$oSourceSet = $oSourceObject->Get($sSourceAttCode);
+			$oSourceSet->Rewind();
+			while ($oSourceLink = $oSourceSet->Fetch())
+			{
+				// Clone the link
+				$sLinkClass = get_class($oSourceLink);
+				$oLinkClone = MetaModel::NewObject($sLinkClass);
+				foreach(MetaModel::ListAttributeDefs($sLinkClass) as $sAttCode => $oAttDef)
+				{
+					// As of now, ignore other attribute (do not attempt to recurse!)
+					if ($oAttDef->IsScalar())
+					{
+						$oLinkClone->Set($sAttCode, $oSourceLink->Get($sAttCode));
+					}
+				}
+
+				// Not necessary - this will be handled by DBObject
+				// $oLinkClone->Set($oSourceAttDef->GetExtKeyToMe(), 0);
+				$oDestSet->AddObject($oLinkClone);
+			}
+			$this->Set($sDestAttCode, $oDestSet);
+		}
+		else
+		{
+			$this->Set($sDestAttCode, $oSourceObject->Get($sSourceAttCode));
+		}
+	}
+
+	/**
+	 * Execute a scripted action onto the current object
+	 *    - clone (att1, att2, att3, ...)
+	 *    - clone_scalars ()
+	 *    - copy (source_att, dest_att)
+	 *    - reset (att)
+	 *    - nullify (att)
+	 *    - set (att, value (placeholders $source->att$ or $current_date$, or $current_contact_id$, ...))
+	 *    - append (att, value (placeholders $source->att$ or $current_date$, or $current_contact_id$, ...))
+	 *    - add_to_list (source_key_att, dest_att)
+	 *    - add_to_list (source_key_att, dest_att, lnk_att, lnk_att_value)
+	 *    - apply_stimulus (stimulus)
+	 *    - call_method (method_name)
+	 *
+	 * @param $sVerb string Any of the verb listed above (e.g. "set")
+	 * @param $aParams array of strings (e.g. array('name', 'copied from $source->name$')
+	 * @param $aSourceObjects Array of Alias => Context objects (Convention: some statements require the 'source' element
+	 * @throws CoreException
+	 * @throws CoreUnexpectedValue
+	 * @throws Exception
+	 */
+	public function ExecAction($sVerb, $aParams, $aSourceObjects)
+	{
+		switch($sVerb)
+		{
+			case 'clone':
+				if (!array_key_exists('source', $aSourceObjects))
+				{
+					throw new Exception('Missing conventional "source" object');
+				}
+				$oObjectToRead = $aSourceObjects['source'];
+				foreach($aParams as $sAttCode)
+				{
+					$this->CopyAttribute($oObjectToRead, $sAttCode, $sAttCode);
+				}
+				break;
+
+			case 'clone_scalars':
+				if (!array_key_exists('source', $aSourceObjects))
+				{
+					throw new Exception('Missing conventional "source" object');
+				}
+				$oObjectToRead = $aSourceObjects['source'];
+				foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef)
+				{
+					if ($oAttDef->IsScalar())
+					{
+						$this->CopyAttribute($oObjectToRead, $sAttCode, $sAttCode);
+					}
+				}
+				break;
+
+			case 'copy':
+				if (!array_key_exists('source', $aSourceObjects))
+				{
+					throw new Exception('Missing conventional "source" object');
+				}
+				$oObjectToRead = $aSourceObjects['source'];
+				if (!array_key_exists(0, $aParams))
+				{
+					throw new Exception('Missing argument #1: source attribute');
+				}
+				$sSourceAttCode = $aParams[0];
+				if (!array_key_exists(1, $aParams))
+				{
+					throw new Exception('Missing argument #2: target attribute');
+				}
+				$sDestAttCode = $aParams[1];
+				$this->CopyAttribute($oObjectToRead, $sSourceAttCode, $sDestAttCode);
+				break;
+
+			case 'reset':
+				if (!array_key_exists(0, $aParams))
+				{
+					throw new Exception('Missing argument #1: target attribute');
+				}
+				$sAttCode = $aParams[0];
+				if (!MetaModel::IsValidAttCode(get_class($this), $sAttCode))
+				{
+					throw new Exception("Unknown attribute ".get_class($this)."::".$sAttCode);
+				}
+				$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
+				$this->Set($sAttCode, $oAttDef->GetDefaultValue());
+				break;
+
+			case 'nullify':
+				if (!array_key_exists(0, $aParams))
+				{
+					throw new Exception('Missing argument #1: target attribute');
+				}
+				$sAttCode = $aParams[0];
+				if (!MetaModel::IsValidAttCode(get_class($this), $sAttCode))
+				{
+					throw new Exception("Unknown attribute ".get_class($this)."::".$sAttCode);
+				}
+				$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
+				$this->Set($sAttCode, $oAttDef->GetNullValue());
+				break;
+
+			case 'set':
+				if (!array_key_exists(0, $aParams))
+				{
+					throw new Exception('Missing argument #1: target attribute');
+				}
+				$sAttCode = $aParams[0];
+				if (!MetaModel::IsValidAttCode(get_class($this), $sAttCode))
+				{
+					throw new Exception("Unknown attribute ".get_class($this)."::".$sAttCode);
+				}
+				if (!array_key_exists(1, $aParams))
+				{
+					throw new Exception('Missing argument #2: value to set');
+				}
+				$sRawValue = $aParams[1];
+				$aContext = array();
+				foreach ($aSourceObjects as $sAlias => $oObject)
+				{
+					$aContext = array_merge($aContext, $oObject->ToArgs($sAlias));
+				}
+				$aContext['current_contact_id'] = UserRights::GetContactId();
+				$aContext['current_contact_friendlyname'] = UserRights::GetUserFriendlyName();
+				$aContext['current_date'] = date('Y-m-d');
+				$aContext['current_time'] = date('H:i:s');
+				$sValue = MetaModel::ApplyParams($sRawValue, $aContext);
+				$this->Set($sAttCode, $sValue);
+				break;
+
+			case 'append':
+				if (!array_key_exists(0, $aParams))
+				{
+					throw new Exception('Missing argument #1: target attribute');
+				}
+				$sAttCode = $aParams[0];
+				if (!MetaModel::IsValidAttCode(get_class($this), $sAttCode))
+				{
+					throw new Exception("Unknown attribute ".get_class($this)."::".$sAttCode);
+				}
+				if (!array_key_exists(1, $aParams))
+				{
+					throw new Exception('Missing argument #2: value to append');
+				}
+				$sRawAddendum = $aParams[1];
+				$aContext = array();
+				foreach ($aSourceObjects as $sAlias => $oObject)
+				{
+					$aContext = array_merge($aContext, $oObject->ToArgs($sAlias));
+				}
+				$aContext['current_contact_id'] = UserRights::GetContactId();
+				$aContext['current_contact_friendlyname'] = UserRights::GetUserFriendlyName();
+				$aContext['current_date'] = date('Y-m-d');
+				$aContext['current_time'] = date('H:i:s');
+				$sAddendum = MetaModel::ApplyParams($sRawAddendum, $aContext);
+				$this->Set($sAttCode, $this->Get($sAttCode).$sAddendum);
+				break;
+
+			case 'add_to_list':
+				if (!array_key_exists('source', $aSourceObjects))
+				{
+					throw new Exception('Missing conventional "source" object');
+				}
+				$oObjectToRead = $aSourceObjects['source'];
+				if (!array_key_exists(0, $aParams))
+				{
+					throw new Exception('Missing argument #1: source attribute');
+				}
+				$sSourceKeyAttCode = $aParams[0];
+				if (!MetaModel::IsValidAttCode(get_class($oObjectToRead), $sSourceKeyAttCode))
+				{
+					throw new Exception("Unknown attribute ".get_class($oObjectToRead)."::".$sSourceKeyAttCode);
+				}
+				if (!array_key_exists(1, $aParams))
+				{
+					throw new Exception('Missing argument #2: target attribute (link set)');
+				}
+				$sTargetListAttCode = $aParams[1]; // indirect !!!
+				if (!MetaModel::IsValidAttCode(get_class($this), $sTargetListAttCode))
+				{
+					throw new Exception("Unknown attribute ".get_class($this)."::".$sTargetListAttCode);
+				}
+				if (isset($aParams[2]) && isset($aParams[3]))
+				{
+					$sRoleAttCode = $aParams[2];
+					$sRoleValue = $aParams[3];
+				}
+
+				$iObjKey = $oObjectToRead->Get($sSourceKeyAttCode);
+				if ($iObjKey > 0)
+				{
+					$oLinkSet = $this->Get($sTargetListAttCode);
+
+					$oListAttDef = MetaModel::GetAttributeDef(get_class($this), $sTargetListAttCode);
+					$oLnk = MetaModel::NewObject($oListAttDef->GetLinkedClass());
+					$oLnk->Set($oListAttDef->GetExtKeyToRemote(), $iObjKey);
+					if (isset($sRoleAttCode))
+					{
+						if (!MetaModel::IsValidAttCode(get_class($oLnk), $sRoleAttCode))
+						{
+							throw new Exception("Unknown attribute ".get_class($oLnk)."::".$sRoleAttCode);
+						}
+						$oLnk->Set($sRoleAttCode, $sRoleValue);
+					}
+					$oLinkSet->AddObject($oLnk);
+					$this->Set($sTargetListAttCode, $oLinkSet);
+				}
+				break;
+
+			case 'apply_stimulus':
+				if (!array_key_exists(0, $aParams))
+				{
+					throw new Exception('Missing argument #1: stimulus');
+				}
+				$sStimulus = $aParams[0];
+				if (!in_array($sStimulus, MetaModel::EnumStimuli(get_class($this))))
+				{
+					throw new Exception("Unknown stimulus ".get_class($this)."::".$sStimulus);
+				}
+				$this->ApplyStimulus($sStimulus);
+				break;
+
+			case 'call_method':
+				if (!array_key_exists('source', $aSourceObjects))
+				{
+					throw new Exception('Missing conventional "source" object');
+				}
+				$oObjectToRead = $aSourceObjects['source'];
+				if (!array_key_exists(0, $aParams))
+				{
+					throw new Exception('Missing argument #1: method name');
+				}
+				$sMethod = $aParams[0];
+				$aCallSpec = array($this, $sMethod);
+				if (!is_callable($aCallSpec))
+				{
+					throw new Exception("Unknown method ".get_class($this)."::".$sMethod.'()');
+				}
+				// Note: $oObjectToRead has been preserved when adding $aSourceObjects, so as to remain backward compatible with methods having only 1 parameter ($oObjectToReadà
+				call_user_func($aCallSpec, $oObjectToRead, $aSourceObjects);
+				break;
+
+			default:
+				throw new Exception("Invalid verb");
+		}
+	}
 }
 

+ 154 - 0
test/testlist.inc.php

@@ -4894,4 +4894,158 @@ class TestLinkSetRecording_1NAdd_Remove extends TestBizModel
 		sort($aRet);
 		return $aRet;
 	}
+}
+
+class TestExecActions extends TestBizModel
+{
+	static public function GetName()
+	{
+		return 'Scripted actions API DBObject::ExecAction - syntax errors';
+	}
+
+	static public function GetDescription()
+	{
+		return 'Check that wrong arguments are correclty reported';
+	}
+
+	protected function DoExecute()
+	{
+		$oSource = new UserRequest();
+		$oSource->Set('title', 'Houston!');
+		$oSource->Set('description', 'Looks like we have a problem');
+
+		$oTarget = new Server();
+
+		////////////////////////////////////////////////////////////////////////////////
+		// Scenarii
+		//
+		$aScenarii = array(
+			array(
+				'action' => 'set',
+				'error' => 'Action: set - Invalid syntax'
+			),
+			array(
+				'action' => 'smurf()',
+				'error' => 'Action: smurf() - Invalid verb'
+			),
+			array(
+				'action' => ' smurf () ',
+				'error' => 'Action:  smurf ()  - Invalid syntax'
+			),
+			array(
+				'action' => 'clone(some_att_code, another_one)',
+				'error' => 'Action: clone(some_att_code, another_one) - Unknown attribute Server::some_att_code'
+			),
+			array(
+				'action' => 'copy(toto, titi)',
+				'error' => 'Action: copy(toto, titi) - Unknown attribute Server::titi'
+			),
+			array(
+				'action' => 'copy(toto, name)',
+				'error' => 'Action: copy(toto, name) - Unknown attribute UserRequest::toto'
+			),
+			array(
+				'action' => 'copy()',
+				'error' => 'Action: copy() - Missing argument #1: source attribute'
+			),
+			array(
+				'action' => 'copy(title)',
+				'error' => 'Action: copy(title) - Missing argument #2: target attribute'
+			),
+			array(
+				'action' => 'set(toto)',
+				'error' => 'Action: set(toto) - Unknown attribute Server::toto'
+			),
+			array(
+				'action' => 'set(toto, something)',
+				'error' => 'Action: set(toto, something) - Unknown attribute Server::toto'
+			),
+			array(
+				'action' => 'set()',
+				'error' => 'Action: set() - Missing argument #1: target attribute'
+			),
+			array(
+				'action' => 'reset(toto)',
+				'error' => 'Action: reset(toto) - Unknown attribute Server::toto'
+			),
+			array(
+				'action' => 'reset()',
+				'error' => 'Action: reset() - Missing argument #1: target attribute'
+			),
+			array(
+				'action' => 'nullify(toto)',
+				'error' => 'Action: nullify(toto) - Unknown attribute Server::toto'
+			),
+			array(
+				'action' => 'nullify()',
+				'error' => 'Action: nullify() - Missing argument #1: target attribute'
+			),
+			array(
+				'action' => 'append(toto, something)',
+				'error' => 'Action: append(toto, something) - Unknown attribute Server::toto'
+			),
+			array(
+				'action' => 'append(name)',
+				'error' => 'Action: append(name) - Missing argument #2: value to append'
+			),
+			array(
+				'action' => 'append()',
+				'error' => 'Action: append() - Missing argument #1: target attribute'
+			),
+			array(
+				'action' => 'add_to_list(toto, titi)',
+				'error' => 'Action: add_to_list(toto, titi) - Unknown attribute UserRequest::toto'
+			),
+			array(
+				'action' => 'add_to_list(caller_id, titi)',
+				'error' => 'Action: add_to_list(caller_id, titi) - Unknown attribute Server::titi'
+			),
+			array(
+				'action' => 'add_to_list(caller_id)',
+				'error' => 'Action: add_to_list(caller_id) - Missing argument #2: target attribute (link set)'
+			),
+			array(
+				'action' => 'add_to_list()',
+				'error' => 'Action: add_to_list() - Missing argument #1: source attribute'
+			),
+			array(
+				'action' => 'apply_stimulus(toto)',
+				'error' => 'Action: apply_stimulus(toto) - Unknown stimulus Server::toto'
+			),
+			array(
+				'action' => 'apply_stimulus()',
+				'error' => 'Action: apply_stimulus() - Missing argument #1: stimulus'
+			),
+			array(
+				'action' => 'call_method(toto)',
+				'error' => 'Action: call_method(toto) - Unknown method Server::toto()'
+			),
+			array(
+				'action' => 'call_method()',
+				'error' => 'Action: call_method() - Missing argument #1: method name'
+			),
+		);
+
+		foreach ($aScenarii as $aScenario)
+		{
+			echo "<h4>".htmlentities($aScenario['action'], ENT_QUOTES, 'UTF-8')."</h4>\n";
+			$sMessage = '';
+			try
+			{
+				$oTarget->ExecActions(array($aScenario['action']), array('source' => $oSource));
+				$sMessage = 'Expecting an exception... none has been thrown!';
+			}
+			catch (Exception $e)
+			{
+				if ($e->getMessage() != $aScenario['error'])
+				{
+					$sMessage = 'Wrong message: expecting "'.$aScenario['error'].'" and got "'.$e->getMessage().'"';
+				}
+			}
+			if ($sMessage !='')
+			{
+				throw new Exception($sMessage);
+			}
+		}
+	}
 }