- converted class names and directories for PSR0 - replaced the basic __autoload() by the canonical PSR0 loader example. - added a GraphViz-based Grapher class to plot Machine structures.
277 lines
7.4 KiB
PHP
277 lines
7.4 KiB
PHP
<?php
|
|
/**
|
|
* Input converter for FSM from Dia
|
|
*
|
|
* This class converts an UML diagram from Dia 0.9.6 into an abstract FSM graph
|
|
* which can then be output by another class to the FSM XML format used by
|
|
* the OSInet FSM 1.6.
|
|
*
|
|
* @copyright (c) 2007-2012 Ouest Systèmes Informatiques
|
|
* @author Frederic G. MARAND
|
|
* @license CeCILL 2.0
|
|
* @link http://wiki.audean.com/fsm/fsm
|
|
* @since FSM 1.6
|
|
*/
|
|
|
|
namespace OSInet\Finite_State_Machine;
|
|
|
|
$erFsmFromDia = error_reporting(E_ALL|E_STRICT);
|
|
|
|
/**
|
|
* This class converts an UML diagram from Dia 0.9.6 into an abstract FSM graph.
|
|
*
|
|
* @todo Validate the diagram: currently it will just choke on non-accepted diagrams.
|
|
*/
|
|
class DiaLoader {
|
|
const DIA_NAMESPACE = "http://www.lysator.liu.se/~alla/dia/";
|
|
|
|
/**
|
|
* The DOM for the source Dia diagram.
|
|
*
|
|
* @var \DOMDocument
|
|
*/
|
|
protected $dom;
|
|
|
|
/**
|
|
* The FSM writer instance to save to.
|
|
*
|
|
* @var Writer
|
|
*/
|
|
protected $fsm;
|
|
|
|
/**
|
|
* Sequence generator for unnamed event results (guards).
|
|
*
|
|
* @var integer
|
|
*/
|
|
protected $resultGenerator = 0;
|
|
|
|
/**
|
|
* Extract the initial/final status from a Dia "UML - State Term".
|
|
*
|
|
* @param DOMElement $element
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function getStateTermInfo($element) {
|
|
$query = 'dia:attribute[@name="is_final"]/dia:boolean';
|
|
|
|
$xpath = new \DOMXPath($this->dom);
|
|
$xpath->registerNamespace("dia", self::DIA_NAMESPACE);
|
|
|
|
$id = $element->getAttribute('id');
|
|
$diaAttributes = $xpath->query($query, $element);
|
|
// Normal case
|
|
if ($diaAttributes->length == 1) {
|
|
// "is_final" is a Dia boolean
|
|
$diaBoolean = $diaAttributes->item(0);
|
|
$val = $diaBoolean->getAttribute('val');
|
|
switch ($val) {
|
|
case 'true':
|
|
$ret = Machine::FINAL_STATE;
|
|
break;
|
|
|
|
case 'false':
|
|
$ret = Machine::INIT_STATE;
|
|
break;
|
|
|
|
default:
|
|
$ret = "anomalous($val)";
|
|
break;
|
|
}
|
|
}
|
|
else {
|
|
echo "Initial/final state #$id does not bear the is_final attribute: anomaly.\n";
|
|
}
|
|
|
|
return array('type' => $ret, 'name' => $ret);
|
|
}
|
|
|
|
/**
|
|
* Extract the name from a Dia "UML - State".
|
|
*
|
|
* @param DOMElement $element
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function getStateInfo($element) {
|
|
$query = 'dia:attribute[@name="text"]/dia:composite/dia:attribute[@name="string"]/dia:string';
|
|
$xpath = new \DOMXPath($this->dom);
|
|
$xpath->registerNamespace("dia", self::DIA_NAMESPACE);
|
|
|
|
$id = $element->getAttribute('id');
|
|
$diaString = $xpath->query($query, $element);
|
|
// Normal case
|
|
if ($diaString->length == 1) {
|
|
$diaText = $diaString->item(0);
|
|
$ret = trim($diaText->textContent, '#');
|
|
}
|
|
else {
|
|
echo "Standard state #$id does not contain the expected content: anomaly.\n";
|
|
}
|
|
return array('type' => 'standard', 'name' => $ret);
|
|
}
|
|
|
|
/**
|
|
* Extract the actual text content from a query for a Dia string element.
|
|
*
|
|
* This is to be used over XPath query results
|
|
*
|
|
* @param DOMNodeList $nodes
|
|
*
|
|
* @return string
|
|
*/
|
|
private function getTextFromDiaString ($nodes) {
|
|
if ($nodes->length == 1) {
|
|
$ret = $nodes->item(0);
|
|
$ret = trim($ret->textContent, '#');
|
|
}
|
|
else {
|
|
$ret = NULL;
|
|
}
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Extract the various information fields from a Dia "UML - Connection".
|
|
*
|
|
* @param DOMElement $element
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function getTransitionInfo($element) {
|
|
$xpath = new \DOMXPath($this->dom);
|
|
$xpath->registerNamespace("dia", self::DIA_NAMESPACE );
|
|
$id = $element->getAttribute('id');
|
|
|
|
$baseQuery = 'dia:attribute[@name="%s"]/dia:string';
|
|
foreach (array('trigger', 'action', 'guard') as $parameter) {
|
|
$query = sprintf($baseQuery, $parameter);
|
|
$nodes = $xpath->query($query, $element);
|
|
$ret[$parameter] = $this->getTextFromDiaString($nodes);
|
|
// Triggers and actions.
|
|
if ($parameter <> 'guard') {
|
|
$ret[$parameter] = ucfirst($ret[$parameter]);
|
|
}
|
|
}
|
|
|
|
$query = 'dia:connections/dia:connection';
|
|
$diaConnections = $xpath->query($query, $element);
|
|
// echo "Connections: {$dia_connections->length}\n";
|
|
if ($diaConnections->length == 2) {
|
|
// echo "Transition $id links" ;
|
|
foreach ($diaConnections as $end) {
|
|
$handle = $end->getAttribute('handle');
|
|
$link = $end->getAttribute('to');
|
|
switch ($handle) {
|
|
case '0' :
|
|
$kind = 'from';
|
|
$ret['from'] = $link ;
|
|
break;
|
|
|
|
case '1' :
|
|
$kind = 'to';
|
|
$ret['to'] = $link ;
|
|
break;
|
|
|
|
default:
|
|
$kind = "anomaly($handle)";
|
|
break;
|
|
}
|
|
// echo " $kind $link";
|
|
}
|
|
}
|
|
else {
|
|
echo "Anomaly detected on the connection properties of transition #$id.\n";
|
|
}
|
|
// echo " on ${trigger}[$guard]/$action.\n";
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Load a Dia file into the DOM
|
|
*
|
|
* @param string $filePath
|
|
*
|
|
* @return void
|
|
*/
|
|
public function __construct($filePath) {
|
|
$this->dom = new \DOMDocument();
|
|
$this->dom->load($filePath);
|
|
$this->fsm = new Writer();
|
|
}
|
|
|
|
/**
|
|
* Parse the DOM to extract the various UML elements to an abstract FSM array.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function parse() {
|
|
$query = '//dia:object';
|
|
$xpath = new \DOMXPath($this->dom);
|
|
$xpath->registerNamespace("dia", self::DIA_NAMESPACE);
|
|
$result = $xpath->query($query);
|
|
|
|
foreach ($result as $object) {
|
|
$type = $object->getAttribute('type');
|
|
$id = $object->getAttribute('id');
|
|
switch ($type) {
|
|
case 'UML - State Term':
|
|
$state = $this->getStateTermInfo($object);
|
|
$this->fsm->addState($state['name']);
|
|
$this->states[$id] = $state; // needed to match transitions
|
|
if (($state['type'] == Machine::INIT_STATE) || ($state['type'] == Machine::FINAL_STATE)) {
|
|
$this->fsm->setSpecialState($state['type'], $state['name']);
|
|
}
|
|
break;
|
|
|
|
case 'UML - State':
|
|
$state = $this->getStateInfo($object);
|
|
$this->fsm->addState($state['name']);
|
|
// Needed to match transitions.
|
|
$this->states[$id] = $state;
|
|
break;
|
|
|
|
case 'UML - Transition':
|
|
$transition = $this->getTransitionInfo($object);
|
|
$stateName = $this->states[$transition['from']]['name'];
|
|
$nextStateName = $this->states[$transition['to']]['name'];
|
|
$eventName = $transition['trigger'];
|
|
$resultName = $transition['guard'];
|
|
$actionName = $transition['action'];
|
|
|
|
// Not allowed.
|
|
if (empty($resultName)) {
|
|
$resultName = "unnnamed_result_" . $this->resultGenerator++;
|
|
}
|
|
|
|
// This add will fail when adding outcomes to existing events, but
|
|
// this is as designed.
|
|
$this->fsm->addEvent($stateName, $eventName);
|
|
|
|
$this->fsm->addOutcome($stateName, $eventName, $resultName,
|
|
$nextStateName, $actionName);
|
|
break;
|
|
|
|
default:
|
|
echo "Object #$id is of unknown type $type: ignored.\n";
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Facade for Writer.
|
|
*
|
|
* @param string $prefix
|
|
* @param boolean $php
|
|
* @param boolean $overwrite
|
|
*/
|
|
public function saveFsm($prefix = 'fsm', $php = FALSE, $overwrite = FALSE) {
|
|
$this->fsm->saveFsm($prefix, $php, $overwrite);
|
|
}
|
|
}
|
|
|
|
|
|
error_reporting($erFsmFromDia);
|
|
unset ($erFsmFromDia);
|