<?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  2007 Ouest Systèmes Informatiques
 * @author     Frederic G. MARAND
 * @license    CeCILL 2.0
 * @version    CVS: $Id: Fsm_From_Dia.php,v 1.2 2007-06-10 19:38:14 marand Exp $
 * @link       http://wiki.audean.com/fsm/fsm
 * @since      FSM 1.6
 * @package    fsm
 * @subpackage fsm.ui
*/

$erFsmFromDia = error_reporting(E_ALL|E_STRICT);

/**
 * This class converts an UML diagram from Dia 0.9.6
 * into an abstract FSM graph.
 *
 * @package    fsm
 * @subpackage fsm.ui
 * @todo Validate the diagram: currently it will just choke on non-accepted diagrams
 */
class Fsm_From_Dia
  {
  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 FSM_Writer
   */
  protected $fsm;

  /**
   * Sequence generator for unnamed event results (guards)
   *
   * @var integer
   */
  protected $result_generator = 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", Fsm_From_Dia::DIA_NAMESPACE);

    $id = $element->getAttribute('id');
    $dia_attributes = $xpath->query($query, $element);
    if ($dia_attributes->length == 1) // Normal case
      {
      $dia_boolean = $dia_attributes->item(0); // "is_final" is a Dia boolean
      $val = $dia_boolean->getAttribute('val');
      switch ($val)
        {
        case 'true':  $ret = Finite_State_Machine::FINAL_STATE; break;
        case 'false': $ret = Finite_State_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", Fsm_From_Dia::DIA_NAMESPACE);

    $id = $element->getAttribute('id');
    $dia_string = $xpath->query($query, $element);
    if ($dia_string->length == 1) // Normal case
      {
      $dia_text = $dia_string->item(0);
      $ret = trim($dia_text->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", Fsm_From_Dia::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);
      if ($parameter <> 'guard') // triggers and actions
        {
        $ret[$parameter] = ucfirst($ret[$parameter]);
        }
      }

    $query = 'dia:connections/dia:connection';
    $dia_connections = $xpath->query($query, $element);
    // echo "Connections: $dia_connections->length\n";
    if ($dia_connections->length == 2)
      {
      // echo "Transition $id links" ;
      foreach ($dia_connections 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 transiition #$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 Fsm_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", Fsm_From_Dia::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'] == Finite_State_Machine::INIT_STATE)
            || ($state['type'] == Finite_State_Machine::FINAL_STATE)
            )
            {
            $this->fsm->setSpecialState($state['type'], $state['name']);
            }
          break;

        case 'UML - State':
          $state = $this->getStateInfo($object);
          $this->fsm->addState($state['name']);
          $this->states[$id] = $state; // needed to match transitions
          break;

        case 'UML - Transition':
          $transition  = $this->getTransitionInfo($object);
          $state_name      = $this->states[$transition['from']]['name'];
          $next_state_name = $this->states[$transition['to']]['name'];
          $event_name  = $transition['trigger'];
          $result_name = $transition['guard'];
          $action_name = $transition['action'];

          if (empty($result_name)) // Not allowed
            {
            $result_name = "unnnamed_result_" . $this->result_generator++;
            }

          /**
           * This add will fail when adding outcomes to existing events,
           * but this is as designed
           */
          $this->fsm->addEvent($state_name, $event_name);


          $this->fsm->addOutcome($state_name, $event_name, $result_name,
            $next_state_name, $action_name);
          break;

        default:
          echo "Object #$id is of unknown type $type: ignored.\n";
          break;
        }
      }

    }

  /**
   * Facade for FSM_Writer
   *
   * @param string $prefix
   * @param boolean $php
   * @param boolean $overwrite
   */
  public function save_fsm($prefix = 'fsm', $php = FALSE, $overwrite = FALSE)
    {
    $ret = $this->fsm->save_fsm($prefix = 'fsm', $php = FALSE, $overwrite = FALSE);
    return $ret;
    }
  }


error_reporting($erFsmFromDia);
unset ($erFsmFromDia);