Browse Source

FSM: PSR-0 conversion, Grapher added.

- 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.
Frederic G. MARAND 12 years ago
parent
commit
a28ed41155

+ 9 - 16
Background_Application.php

@@ -6,13 +6,9 @@
  *
  * This version relies on OSInet FSM >= 1.6
  *
- * @copyright  (c) 2007 OSI
+ * @copyright  (c) 2007-2012 OSI
  * @license    Licensed under the CeCILL 2.0
- * @version    CVS: $Id: Background_Application.php,v 1.4 2007-06-10 19:39:54 marand Exp $
  * @link       http://wiki.audean.com/fsm/fsm
- * @since      Not applicable yet
- * @package    fsm
- * @subpackage fsm.ui
  */
 
 error_reporting(E_ALL|E_STRICT);
@@ -22,11 +18,8 @@ error_reporting(E_ALL|E_STRICT);
  * based on an OSInet FSM (finite state machine).
  * Concrete implementations should include a constructor defining the
  * backgroundGoals array along the FSM graph.
- * @package    fsm
- * @subpackage fsm.ui
  */
-abstract class Background_Application
-  {
+abstract class Background_Application {
   /**
    * Trace operation to stdout
    *
@@ -36,7 +29,7 @@ abstract class Background_Application
 
   /**
    * This is the FSM sequencing the background operation
-   * @var Finite_State_Machine
+   * @var Machine
    */
   public $finiteStateMachine;
 
@@ -94,13 +87,13 @@ abstract class Background_Application
       $msg = "Nothing to do for now. Stop idling";
       $ret = FALSE;
       }
-    elseif ($this->finiteStateMachine->get_state() != $this->backgroundGoals[$event])
+    elseif ($this->finiteStateMachine->getState() != $this->backgroundGoals[$event])
       {
-      $msg .= "state " . $this->finiteStateMachine->get_state() . "($event)";
-      $result = $this->finiteStateMachine->apply_event($event);
-      $msg .= "[$result->fsm_return] => $result->fsm_state";
-      if (!empty($result->fsm_action))
-        $msg .= " / $result->fsm_action";
+      $msg .= "state " . $this->finiteStateMachine->getState() . "($event)";
+      $result = $this->finiteStateMachine->applyEvent($event);
+      $msg .= "[$result->fsmReturn] => $result->fsmState";
+      if (!empty($result->fsmAction))
+        $msg .= " / $result->fsmAction";
       }
     else
       {

+ 0 - 480
Finite_State_Machine.php

@@ -1,480 +0,0 @@
-<?php
-/**
- * Finite state machine skeleton
- *
- * @copyright  (c) 2007 OSI
- * @author     Frédéric G. MARAND
- * @license    Licensed under the CeCILL 2.0
- * @version    CVS: $Id: Finite_State_Machine.php,v 1.9 2007-06-10 19:39:54 marand Exp $
- * @link       http://wiki.audean.com/fsm/fsm
- * @since      Not applicable yet
- * @package    fsm
- * @subpackage fsm.core
- * @todo       replace setAttribute('id',...) by setIdAttribute when PHP5.2 becomes mandatory
- */
-
-/**
- * needed notably for autoload and func_name()
- */
-require_once('misc.php'); // for func_name()
-
-$erFiniteStateMachine = error_reporting(E_ALL|E_STRICT);
-
-/**
- * This class defines a possible outcome for a given FSM transition
- * @package    fsm
- * @subpackage fsm.core
- */
-class Fsm_Result
-  {
-  /**
-   * The return value of the event handler
-   * @var mixed
-   */
-  public $fsm_return;
-
-  /**
-   * The name of the state to which the FSM must change. If NULL, do not change
-   * the current state.
-   *
-   * @var string
-   */
-  public $fsm_state;
-
-  /**
-   * The name of an event to be fired after the state change has been applied
-   *
-   * @var string
-   */
-  public $fsm_action;
-
-  /**
-   * @param string $state
-   * @param string $action
-   * @return void
-   */
-  public function __construct($return = NULL, $state = NULL, $action = NULL)
-    {
-    $this->fsm_return = $return;
-    $this->fsm_state = $state;
-    $this->fsm_action = $action;
-    }
-  }
-
-/**
- * This class must be inherited by code implementing actual FSMs.
- *
- * Applications must create a fsm descendant in which they will:
- *
- *   - define the f_transitions table, usually in their constructor
- *   - invoke the parent constructor to set up the FSM from the transitions table
- *   - create "f_foo" functions for each "foo" event, except "idle", which is builtin.
- *
- * Applications may:
- *   - disable event processing by setting $this->event to one of the fsm::EVENT_* constants
- *   - disable post-event actions by setting $this->allows actions to false
- *   - disable the builtin idle event by setting $this->idle_work to false
- *   - query the current state by using $this->get_state()
- *   - send an idle event by using $this->idle()
- *   - submit any event (including idle) by using $this->apply_event($event_name)
- *
- * @package    fsm
- * @subpackage fsm.core
- */
-abstract class Finite_State_Machine
-  {
-  const VERSION      = '$Id';
-  const IDLE_EVENT   = 'idle'; // must not change name: method f_idle depends on it
-
-  const INIT_STATE   = 'init';
-  const FINAL_STATE  = 'final';
-
-  const EVENT_NORMAL = 1; // processes events
-  const EVENT_QUEUE  = 2; // queue events for later use
-  const EVENT_SINK   = 3; // throw away events
-
-  public $idle_work     = TRUE;
-  public $allow_actions = TRUE;
-
-  protected $f_event_mode = Finite_State_Machine::EVENT_NORMAL;
-  protected $f_queue      = array(); // event queue for EVENT_QUEUE mode
-
-  /**
-   * the current state of the object
-   * @var string
-   */
-  protected $f_state;
-
-  /**
-   * Transitions holds the transitions table
-   * state1
-   *   event1
-   *     result1
-   *     state_name|Fsm_Result
-   *   event2
-   *     ...
-   *   ..
-   * ..
-   * @var array
-   */
-  protected $f_transitions = null;
-
-  /**
-   * constructor initializes the FSM to the first
-   * state in the transitions table
-   * @return void
-   */
-  public function __construct()
-    {
-    $this->_check_transitions();
-
-    reset($this->f_transitions);
-    $x = each($this->f_transitions);
-    $x = $x[0];
-    $this->f_state = $x;
-    }
-
-  /**
-   * make sure a transitions graph has been defined
-   * @return void
-   */
-  protected function _check_transitions()
-    {
-    if (!isset($this->f_transitions))
-      throw new Exception('No FSM processing is allowed without a transitions table\n');
-    }
-
-  /**
-   * getter for f_state
-   * @return string
-   */
-  public function get_state()
-    {
-    return $this->f_state;
-    }
-
-  /**
-   * return the list of events accepted in the current state
-   * @return array
-   */
-  public function get_accepted_events()
-    {
-    $this->_check_transitions();
-
-    try
-      {
-      $events = array_keys($this->f_transitions[$this->f_state]);
-      // echo func_name() . ": state $this->f_state, accepted events are:\n" . print_r($events, true). "\n";
-      }
-    catch (Exception $e)
-      {
-      echo "Exception in get_accepted_events" . print_r($e);
-      print_r(debug_backtrace());
-      $events = array();
-      }
-
-    return $events;
-    }
-
-  /**
-   * return the list of outcome accepted in the current state for the give event
-   * @param string $event_name
-   * @param mixed $outcome
-   * @return array
-   */
-  public function get_accepted_outcomes($event_name)
-    {
-    // echo func_name() . "\n";
-    $this->_check_transitions();
-
-    /**
-     * Spare some paranioa
-     *
-    if (!$this->is_event_allowed($event_name))
-      throw new Exception(func_name() . ": event \"$event_name\" not allowed in state \"$this->f_state\"\n");
-     */
-
-    $outcomes = array_keys($this->f_transitions[$this->f_state][$event_name]);
-    // print_r($this->f_transitions[$this->f_state][$event_name]);
-    // echo "outcomes for event $event_name: " . var_dump($outcomes) . "\n";
-    return $outcomes;
-    }
-
-  /**
-   * is this event accepted in the current state
-   * the FSM is in ?
-   *
-   * @param string $event_name
-   * @return boolean
-   */
-  public function is_event_allowed($event_name)
-    {
-    // echo func_name() . "($event_name)";
-    $this->_check_transitions();
-
-    $ret = in_array($event_name, $this->get_accepted_events());
-    // echo " in state $this->f_state, result = <$ret>\n";
-    return $ret;
-    }
-
-  /**
-   * is a given outcome available for a given event,
-   * considering the current context ?
-   *
-   * @param string $event_name
-   * @param mixed $outcome
-   * @return boolean
-   */
-  public function is_outcome_allowed($event_name, $outcome)
-    {
-    $this->_check_transitions();
-
-    if (!$this->is_event_allowed($event_name))
-      return false;
-
-    $ret = array_key_exists($outcome, $this->get_accepted_outcomes($event_name));
-    return $ret;
-    }
-
-  /**
-   * apply an event, and the resulting event chain if needed
-   *
-   * @param string $event_name
-   * @param array $params the
-   * @return Fsm_Result resulting state
-   */
-  public function apply_event($event_name)
-    {
-    // echo "Start of " . func_name() . "\n";
-
-    do {
-      $result = $this->apply_simple_event($event_name);
-      if ($this->allow_actions)
-        {
-        $event_name = $result->fsm_action; // can be NULL
-        }
-      else
-        {
-        $event_name = NULL;
-        }
-      } while($event_name);
-    // echo "End of " . func_name() . "\n";
-    return $result;
-    }
-
-  /**
-   * Helper for apply_event that does not implement the post-transition action
-   *
-   * @param string $event_name
-   * @return Fsm_Result
-   * @see apply_event()
-   */
-  private function apply_simple_event($event_name)
-    {
-    // echo "Start of " . func_name() . ", event = $event_name\n";
-    $current_state = $this->f_state;
-    if (($event_name == Finite_State_Machine::IDLE_EVENT) && !$this->idle_work)
-      {
-      return new Fsm_Result();
-      }
-
-    if (!$this->is_event_allowed($event_name))
-      {
-      die("Event $event_name not allowed in current state $current_state.\n");
-      /* throw new Exception(func_name()
-        . ":  Event \"$event_name\" not accepted in current state \"$current_state\"");
-      */
-      }
-
-    $outcomes = $this->get_accepted_outcomes($event_name);
-
-    $method_name = "event$event_name";
-    if (!method_exists($this, $method_name))
-      {
-      die (func_name() . ": missing method "
-        . get_class($this) . "::$method_name. Aborting.\n");
-      }
-    $outcome = $this->$method_name();
-
-    if (!in_array($outcome, $outcomes))
-      {
-      throw new Exception(func_name()
-        . ": event guard. Transition on \"$event_name\" return invalid outcome: "
-        . print_r($outcome, true)
-        . " for state $this->f_state\n");
-      }
-
-    $transition = &$this->f_transitions[$current_state][$event_name][$outcome];
-    $result = new Fsm_Result
-      (
-      $outcome,
-      $transition[0],
-      $transition[1]
-      );
-    if (isset($result->fsm_state))
-      {
-      $this->f_state = $result->fsm_state;
-      }
-    /* print_r($this->f_transitions[$current_state][$event_name]);
-    var_dump($result); */
-    if (!isset($outcome))
-      $outcome = 'NULL';
-    /*
-    echo func_name()
-      . ": $current_state: " . $event_name . '[' . $outcome . ']'
-      . " -> $this->f_state / " . $result->fsm_action . PHP_EOL;
-    */
-    return $result;
-    }
-
-  /**
-   * Default event
-   * @return boolean
-   */
-  public function f_idle()
-    {
-    return TRUE;
-    }
-
-  /**
-   * Apply an fsm::IDLE_EVENT event. Do not confuse with f_idle !
-   *
-   * @return Fsm_Result
-   */
-  public function idle()
-    {
-    return $this->apply_event(Finite_State_Machine::IDLE_EVENT);
-    }
-
-  /**
-   * return the current operating mode of the FSM
-   * @return int
-   */
-  public function get_event_mode()
-    {
-    return $this->f_event_mode;
-    }
-
-  /**
-   * set the current operating mode for the FSM
-   *
-   * @param int $mode fsm::EVENT_* constants
-   * @return int $mode fsm::EVENT_* constants
-   */
-  public function set_event_mode($mode)
-    {
-    switch ($mode)
-      {
-      case Finite_State_Machine::EVENT_NORMAL :
-        if (count($this->f_queue) > 0)
-          {
-          while (($event = array_shift($this->f_queue)) !== NULL)
-            {
-            $this->apply_event($event);
-            }
-          }
-        break;
-      case Finite_State_Machine::EVENT_QUEUE  : // nothing special to do
-        break;
-      case Finite_State_Machine::EVENT_SINK : // empty queue if needed
-        if (count($this->f_queue) > 0)
-          {
-          $this->f_queue = array();
-          }
-        break;
-      default:
-        throw new Exception("Trying to set unknown FSM mode $mode");
-      }
-    $this->f_event_mode = $mode;
-    return $this->f_event_mode;
-    }
-
-  /**
-   * Load the f_transitions table from an external resource.
-   *
-   * @param string $url Optional: defaults to the name of the class, . ".xml"
-   */
-  public function load_fsm($url = NULL)
-    {
-    $osd = FALSE; // on screen display (debug)
-    if ($osd)
-      echo "Loading FSM from $url\n";
-
-    if (!isset($url))
-      {
-      $url = get_class($this) . ".xml";
-      }
-
-    $fsm = simplexml_load_file($url);
-    $fsm_version = (string) $fsm['fsm_version'];
-    if ($fsm_version !== '1.3')
-      {
-      die("Revision $fsm_version of schema is not supported.\n");
-      }
-
-    $this->idle_work     = ($fsm['idle_work']     == 1);
-    $this->allow_actions = ($fsm['allow_actions'] == 1);
-
-    $this->f_transitions = array();
-    $t = &$this->f_transitions;
-
-    // (string) casts in this loop are required: RHS is a SimpleXMLElement
-    foreach ($fsm->state as $state)
-      {
-      $id = (string) $state['id'];
-      if ($osd)
-        echo "State $id :\n";
-      $t[$id] = array();
-      switch ($id)
-        {
-        case Finite_State_Machine::INIT_STATE :
-          if ($osd)
-            echo "  Initial state\n";
-          break;
-        case Finite_State_Machine::FINAL_STATE :
-          if ($osd)
-            echo "  Final state\n";
-          break;
-        }
-      foreach ($state->event as $event)
-        {
-        $ename = (string) $event['name'];
-        if ($osd)
-          echo "  Event $ename";
-
-        if (!isset($event['type']))
-          $event['type'] = 'void';
-        $etype = (string) $event['type'];
-        if ($osd)
-          echo ", type $etype\n";
-
-        foreach ($event as $next)
-          {
-          if ($event['type'] == 'void')
-            {
-            $next['result'] = 'always';
-            $eresult = null;
-            }
-          else
-            $eresult = (string) $next['result'];
-
-          if (!isset($next['state']))
-            $next['state'] = (string) $state['id'];
-          if ($osd)
-            {
-            echo "    Next(" . $next['result'] . ') = ' . $next['state'];
-            if (isset($next['action']))
-              echo " + event " . $next['action'];
-            echo PHP_EOL;
-            }
-          $t[$id][$ename][$eresult] = array(
-            (string) $next['state'],
-            (string) $next['action']);
-          }
-        }
-      }
-    }
-  }
-
-error_reporting($erFiniteStateMachine);
-unset($erFiniteStateMachine);

+ 0 - 283
Fsm_From_Dia.php

@@ -1,283 +0,0 @@
-<?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);

+ 277 - 0
OSInet/Finite_State_Machine/DiaLoader.php

@@ -0,0 +1,277 @@
+<?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);

+ 46 - 51
Ftp_Client.php → OSInet/Finite_State_Machine/FtpClient.php

@@ -1,46 +1,31 @@
 <?php
+
 /**
  * An FTP transfer wrapper using the OSInet Finite_State_Machine
  *
- * @copyright  (c) 2007 OSI
+ * @copyright  (c) 2007-2012 Ouest Systèmes Informatiques
  * @author     Frédéric G. MARAND
  * @license    Licensed under the CeCILL 2.0
- * @version    CVS: $Id: Ftp_Client.php,v 1.3 2007-06-10 19:39:54 marand Exp $
- * @link
- * @since      Not applicable yet
- * @package    osinetoffice
- * @subpackage bo_up_ingram
- *
  */
 
+namespace OSInet\Finite_State_Machine;
+
 /**
  * Save current reporting level while we set it
  */
 $_ftpEr = error_reporting(E_ALL | E_STRICT);
 
 /**
- * Class implements a finite-state-machine-based
- * FTP client with *very* limited functionality,
- * but that can display progress information
- * states are:
+ * Class implements a finite-state-machine-based FTP client with *very* limited
+ * functionality, but that can display progress information states, namely:
  * - init: not yet set up
  * - offline: no link established
  * - online: client connected to server
  * - live: client connected and logged in
  * - active: a data transfer operation is under way
  * - unsafe: something failed, disconnect can happen out of our control
- *
- * @package    osinetoffice
- * @subpackage bo_up_ingram
  */
-class Ftp_Client extends Finite_State_Machine
-  {
-  /**
-   * Code currently depends on the actual FSM location. Future versions should
-   * probably locate it on their own.
-   */
-  const FTP_FSM = 'e:/src/osinetoffice/lib/Ftp.xml';
-
+class FtpClient extends Machine {
   /**
    * remote properties
    */
@@ -64,23 +49,44 @@ class Ftp_Client extends Finite_State_Machine
   private $fFtpGoal;       // bytes to be transferred
   private $fFtpConnection; // connection
 
+  /**
+   * @return void
+   */
+  public function __construct($values = array()) {
+    if (!isset($values['source'])) {
+      throw \Exception('Unspecified machine description.');
+    }
+    $this->loadFsm($values['source']);
+
+    // print_r($this->fTransitions);
+    parent::__construct();
+    // print_r($this);die();
+  }
+
   /**
    * Initialize parameters from an array
    *
    * @todo should really check names and values.
+   *
    * @param array $params
-   * @return Fsm_Result
+   *
+   * @return Result
    */
-  public function setParameters($params)
-    {
-    foreach ($params as $name => $value)
-      {
+  public function setParameters($params) {
+    $r = new \ReflectionClass(get_class($this));
+    $dp = $r->getDefaultProperties();
+    foreach ($params as $name => $value) {
       $field_name = "f$name";
-      $this->$field_name = $value;
+      if (array_key_exists($field_name, $dp)) {
+        $this->$field_name = $value;
+      }
+      else {
+        echo "Skipping invalid parameter $name.\n";
       }
-    $ret = $this->apply_event('CheckParameters');
-    return $ret;
     }
+    $ret = $this->applyEvent('CheckParameters');
+    return $ret;
+  }
 
   /**
    * ============ utility functions ============
@@ -126,7 +132,7 @@ class Ftp_Client extends Finite_State_Machine
   protected function eventChdir()
     {
     $ret = ftp_chdir($this->fFtpConnection, $this->fRemoteWd);
-    return ftp_client::stringFromBoolean($ret);
+    return self::stringFromBoolean($ret);
     }
 
   /**
@@ -146,7 +152,7 @@ class Ftp_Client extends Finite_State_Machine
       && isset($this->fFtpCallback)
       ;
 
-    $ret = ftp_client::stringFromBoolean($ret);
+    $ret = self::stringFromBoolean($ret);
     // echo func_name() . ", ret = $ret\n";
     return $ret;
     }
@@ -188,7 +194,7 @@ class Ftp_Client extends Finite_State_Machine
     // echo func_name() . "\n";
     $this->fFtpConnection = ftp_connect($this->fRemoteHost); // default port, default timeout
     $ret = is_resource($this->fFtpConnection);
-    return Ftp_Client::stringFromBoolean($ret);
+    return self::stringFromBoolean($ret);
     }
 
   /**
@@ -209,7 +215,7 @@ class Ftp_Client extends Finite_State_Machine
       }
     if ($this->fFtpCallback)
       call_user_func($this->fFtpCallback, $this, 'post');
-    $ret = Ftp_Client::stringFromFtp($ret);
+    $ret = self::stringFromFtp($ret);
     return $ret;
     }
 
@@ -225,7 +231,7 @@ class Ftp_Client extends Finite_State_Machine
     $this->fLocalFp = fopen($this->fLocalFile, "wb");
     if (!is_resource($this->fLocalFp))
       {
-      $ret = Ftp_Client::stringFromFtp(FTP_FAILED);
+      $ret = self::stringFromFtp(FTP_FAILED);
       // throw new Exception(func_name() . ": could not create local file $this->fLocalFile");
       return $ret;
       }
@@ -240,7 +246,7 @@ class Ftp_Client extends Finite_State_Machine
       }
     call_user_func($this->fFtpCallback, $this, 'post');
     // echo func_name() . " => $ret\n"; flush();
-    return Ftp_Client::stringFromFtp($ret);
+    return self::stringFromFtp($ret);
     }
 
   /**
@@ -251,7 +257,7 @@ class Ftp_Client extends Finite_State_Machine
     {
     // echo func_name() . "\n";
     $ret = ftp_login($this->fFtpConnection, $this->fRemoteUser, $this->fRemotePass);
-    return Ftp_Client::stringFromBoolean($ret);
+    return self::stringFromBoolean($ret);
     }
 
   /**
@@ -263,17 +269,6 @@ class Ftp_Client extends Finite_State_Machine
     return; // the FSM needs nothing for this
     }
 
-  /**
-   * @return void
-   */
-  public function __construct()
-    {
-    $this->load_fsm(Ftp_Client::FTP_FSM);
-    // print_r($this->f_transitions);
-    parent::__construct();
-    // print_r($this);die();
-    }
-
   /**
    * close connection if it hasn't been done, to prevent connection
    * lingering on the server if avoidable
@@ -282,9 +277,9 @@ class Ftp_Client extends Finite_State_Machine
   public function __destruct()
     {
     // echo func_name() . PHP_EOL;
-    if ($this->is_event_allowed('Close'))
+    if ($this->isEventAllowed('Close'))
       {
-      $this->apply_event('Close');
+      $this->applyEvent('Close');
       }
 
     if(is_resource($this->fLocalFp))
@@ -321,7 +316,7 @@ class Ftp_Client extends Finite_State_Machine
    */
   public function getProgress()
     {
-    if ((!$this->f_state == 'active') || (!is_resource($this->fLocalFp)))
+    if ((!$this->fState == 'active') || (!is_resource($this->fLocalFp)))
       {
       $ret = 0.0;
       }

+ 138 - 0
OSInet/Finite_State_Machine/Grapher.php

@@ -0,0 +1,138 @@
+<?php
+
+/**
+ * Output converter for FSM from GraphViz
+ *
+ * This class converts a loaded FSM to a GraphViz DOT file.
+ *
+ * @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;
+
+class Grapher {
+  const STATE_SHAPE = 'octagon';
+  const EVENT_SHAPE = 'egg';
+
+  /**
+   * The FSM being plotted.
+   *
+   * @var Machine
+   */
+  public $m;
+
+  /**
+   * The Image_Graphviz canvas.
+   *
+   * @var Image_Graphviz
+   */
+  public $g;
+
+  /**
+   * Debug level. >= 0.
+   *
+   * @var int
+   */
+  public $debug = NULL;
+  /**
+   * A hash of state name to GraphViz name.
+   *
+   * @var array
+   */
+  public $states = array();
+
+  /**
+   * @param Machine $m
+   */
+  public function __construct(Machine $m, $values = array()) {
+    $this->m = $m;
+
+    $default_values = array(
+      'debug' => 0,
+    );
+    $values = array_intersect_key($values, $default_values);
+    $values += $default_values;
+    foreach ($values as $key => $value) {
+      $this->$key = $value;
+    }
+
+    // PEAR is not currently PSR-0 compliant.
+    // As a consequence, strict PSR-0 autoloading will not work for it.
+    require 'Image/GraphViz.php';
+    $this->g = new \Image_Graphviz();
+  }
+
+  public function render() {
+    $i = 0;
+    $g = &$this->g;
+    // First make sure all states have labeled nodes, so that forward
+    // declarations of states occurring in transitions find the proper state id.
+    foreach ($this->m->fTransitions as $state => $events) {
+      $label = $state;
+      $state .= "_$i";
+      $this->states[$label] = $state;
+      $g->addNode($state, array(
+        'label' => $label,
+        'shape' => self::STATE_SHAPE,
+      ));
+      $i++;
+    }
+    foreach ($this->m->fTransitions as $state => $events) {
+      $state_id = $this->states[$state];
+      foreach ($events as $event => $outcomes) {
+        if ($this->debug) {
+          echo "State $state Event $event " . count($outcomes) . " outcomes\n";
+          print_r($outcomes);
+        }
+        if (0 && count($outcomes) == 1) {
+          $outcome = reset($outcomes);
+          // Only one outcome: we do not care for the result.
+          echo "State $state event $event, outcome:"; print_r($outcome); echo "\n";
+          $edge = array($state_id => $this->states[$outcome[0]]);
+          $edge_label = "\n:" . $outcome[1];
+          $edge_attributes = empty($outcome[1])
+            ? array()
+            : array('label' => $edge_label);
+          $g->addEdge($edge, $edge_attributes);
+        }
+        else {
+          $event_label = $event;
+          $event_id = "{$state_id}_$event";
+          $g->addNode($event_id, array(
+            'label' => $event_label,
+            'shape' => self::EVENT_SHAPE,
+          ));
+          $g->addEdge(array($state_id => $event_id));
+          foreach ($outcomes as $result => $next) {
+            list($next_state, $next_action) = $next;
+            $edge = array($event_id => $this->states[$next_state])  ;
+            $edge_label_parts = array();
+            if (!empty($result)) {
+              $edge_label_parts[] = "[$result]";
+            }
+            if (!empty($next_action)) {
+              $edge_label_parts[] = ":$next_action";
+            }
+            if (empty($edge_label_parts)) {
+              echo "No label!\n";
+              $g->addEdge($edge);
+            }
+            else {
+              $edge_label = implode("\n", $edge_label_parts);
+              $edge_attributes = array(
+                'label' => $edge_label,
+              );
+              $g->addEdge($edge, $edge_attributes);
+            }
+          }
+        }
+      }
+    }
+
+    return $g->parse();
+  }
+}

+ 413 - 0
OSInet/Finite_State_Machine/Machine.php

@@ -0,0 +1,413 @@
+<?php
+
+namespace OSInet\Finite_State_Machine;
+
+/**
+ * Abstract state machine.
+ *
+ * @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
+ *
+ * This class must be inherited by code implementing actual FSMs.
+ *
+ * Applications must create a Machine descendant in which they will:
+ *   - define the fTransitions table, usually in their constructor
+ *   - invoke the parent constructor to set up the FSM from the transitions table
+ *   - create "f_foo" functions for each "foo" event, except "idle", which is builtin.
+ *
+ * Applications may:
+ *   - disable event processing by setting $this->event to one of the fsm::EVENT_* constants
+ *   - disable post-event actions by setting $this->allowsActions to false
+ *   - disable the builtin idle event by setting $this->idleWork to false
+ *   - query the current state by using $this->getState()
+ *   - send an idle event by using $this->idle()
+ *   - submit any event (including idle) by using $this->applyEvent($eventName)
+ */
+abstract class Machine {
+  const VERSION      = '12D22';
+
+  const IDLE_EVENT   = 'idle'; // must not change name: method fIdle depends on it
+
+  const INIT_STATE   = 'init';
+  const FINAL_STATE  = 'final';
+
+  const EVENT_NORMAL = 1; // processes events
+  const EVENT_QUEUE  = 2; // queue events for later use
+  const EVENT_SINK   = 3; // throw away events
+
+  public $idleWork     = TRUE;
+  public $allowActions = TRUE;
+
+  protected $fEventMode = self::EVENT_NORMAL;
+  protected $fQueue     = array(); // event queue for EVENT_QUEUE mode
+
+  /**
+   * the current state of the object
+   * @var string
+   */
+  protected $fState;
+
+  /**
+   * Transitions holds the transitions table
+   * state1
+   *   event1
+   *     result1
+   *     state_name|Result
+   *   event2
+   *     ...
+   *   ..
+   * ..
+   * @var array
+   */
+  public $fTransitions = null;
+
+  /**
+   * constructor initializes the FSM to the first
+   * state in the transitions table
+   * @return void
+   */
+  public function __construct() {
+    $this->checkTransitions();
+
+    reset($this->fTransitions);
+    $x = each($this->fTransitions);
+    $x = $x[0];
+    $this->fState = $x;
+  }
+
+  /**
+   * Make sure a transitions graph has been defined
+   *
+   * @return void
+   */
+  protected function checkTransitions() {
+    if (!isset($this->fTransitions))
+      throw new Exception('No FSM processing is allowed without a transitions table\n');
+  }
+
+  /**
+   * Getter for fState
+   *
+   * @return string
+   */
+  public function getState() {
+    return $this->fState;
+  }
+
+  /**
+   * return the list of events accepted in the current state
+   * @return array
+   */
+  public function getAcceptedEvents() {
+    $this->checkTransitions();
+
+    try {
+      $events = array_keys($this->fTransitions[$this->fState]);
+      // echo func_name() . ": state $this->fState, accepted events are:\n" . print_r($events, true). "\n";
+    }
+    catch (Exception $e) {
+      echo "Exception in getAcceptedEvents" . print_r($e);
+      print_r(debug_backtrace());
+      $events = array();
+    }
+
+    return $events;
+  }
+
+  /**
+   * return the list of outcome accepted in the current state for the give event
+   * @param string $eventName
+   * @param mixed $outcome
+   * @return array
+   */
+  public function getAcceptedOutcomes($eventName) {
+    // echo func_name() . "\n";
+    $this->checkTransitions();
+
+    /**
+     * Spare some paranioa
+     *
+     if (!$this->isEventAllowed($eventName))
+       throw new Exception(func_name() . ": event \"$eventName\" not allowed in state \"$this->fState\"\n");
+     */
+
+    $outcomes = array_keys($this->fTransitions[$this->fState][$eventName]);
+    // print_r($this->fTransitions[$this->fState][$eventName]);
+    // echo "outcomes for event $eventName: " . var_dump($outcomes) . "\n";
+    return $outcomes;
+  }
+
+  /**
+   * is this event accepted in the current state
+   * the FSM is in ?
+   *
+   * @param string $eventName
+   * @return boolean
+   */
+  public function isEventAllowed($eventName) {
+    // echo func_name() . "($eventName)";
+    $this->checkTransitions();
+
+    $ret = in_array($eventName, $this->getAcceptedEvents());
+    // echo " in state $this->fState, result = <$ret>\n";
+    return $ret;
+  }
+
+  /**
+   * is a given outcome available for a given event,
+   * considering the current context ?
+   *
+   * @param string $eventName
+   * @param mixed $outcome
+   * @return boolean
+   */
+  public function isOutcomeAllowed($eventName, $outcome) {
+    $this->checkTransitions();
+
+    if (!$this->isEventAllowed($eventName)) {
+      return false;
+    }
+
+    $ret = array_key_exists($outcome, $this->getAcceptedOutcomes($eventName));
+    return $ret;
+  }
+
+  /**
+   * apply an event, and the resulting event chain if needed
+   *
+   * @param string $eventName
+   * @param array $params the
+   * @return Result resulting state
+   */
+  public function applyEvent($eventName) {
+    // echo "Start of " . func_name() . "\n";
+
+    do {
+      $result = $this->applySimpleEvent($eventName);
+      if ($this->allowActions) {
+        $eventName = $result->fsmAction; // can be NULL
+      } else {
+        $eventName = NULL;
+      }
+    } while ($eventName);
+    // echo "End of " . func_name() . "\n";
+    return $result;
+  }
+
+  /**
+   * Helper for applyEvent that does not implement the post-transition action
+   *
+   * @see applyEvent()
+   *
+   * @param string $eventName
+   *
+   * @return Result
+   */
+  private function applySimpleEvent($eventName) {
+    // echo "Start of " . func_name() . ", event = $eventName\n";
+    $currentState = $this->fState;
+    if (($eventName == self::IDLE_EVENT) && !$this->idleWork) {
+      return new Result();
+    }
+
+    if (!$this->isEventAllowed($eventName)) {
+      die("Event $eventName not allowed in current state $currentState.\n");
+      /* throw new Exception(func_name()
+       . ":  Event \"$eventName\" not accepted in current state \"$currentState\"");
+      */
+    }
+
+    $outcomes = $this->getAcceptedOutcomes($eventName);
+
+    $methodName = "event$eventName";
+    if (!method_exists($this, $methodName)) {
+      die (func_name() . ": missing method "
+        . get_class($this) . "::$methodName. Aborting.\n");
+    }
+    $outcome = $this->$methodName();
+
+    if (!in_array($outcome, $outcomes)) {
+      throw new \Exception(func_name()
+        . ": event guard. Transition on \"$eventName\" return invalid outcome: "
+        . print_r($outcome, true)
+        . " for state $this->fState\n");
+    }
+
+    $transition = &$this->fTransitions[$currentState][$eventName][$outcome];
+    $result = new Result (
+        $outcome,
+        $transition[0],
+        $transition[1]
+    );
+    if (isset($result->fsmState)) {
+      $this->fState = $result->fsmState;
+    }
+    /* print_r($this->fTransitions[$currentState][$eventName]);
+     var_dump($result); */
+    if (!isset($outcome)) {
+      $outcome = 'NULL';
+    }
+
+    /*
+     echo func_name()
+    . ": $currentState: " . $eventName . '[' . $outcome . ']'
+    . " -> $this->fState / " . $result->fsmAction . PHP_EOL;
+    */
+    return $result;
+  }
+
+  /**
+   * Default event.
+   *
+   * @return boolean
+   */
+  public function fIdle() {
+    return TRUE;
+  }
+
+  /**
+   * Apply an fsm::IDLE_EVENT event. Do not confuse with fIdle !
+   *
+   * @return Result
+   */
+  public function idle() {
+    return $this->applyEvent(self::IDLE_EVENT);
+  }
+
+  /**
+   * return the current operating mode of the FSM
+   * @return int
+   */
+  public function getEventMode() {
+    return $this->fEventMode;
+  }
+
+  /**
+   * set the current operating mode for the FSM
+   *
+   * @param int $mode fsm::EVENT_* constants
+   * @return int $mode fsm::EVENT_* constants
+   */
+  public function setEventMode($mode) {
+    switch ($mode) {
+      case self::EVENT_NORMAL :
+        if (count($this->fQueue) > 0) {
+          while (($event = array_shift($this->fQueue)) !== NULL) {
+            $this->applyEvent($event);
+          }
+        }
+        break;
+
+      // Nothing special to do
+      case self::EVENT_QUEUE:
+        break;
+
+      // Empty queue if needed
+      case self::EVENT_SINK :
+        if (count($this->fQueue) > 0) {
+          $this->fQueue = array();
+        }
+        break;
+
+      default:
+        throw new Exception("Trying to set unknown FSM mode $mode");
+    }
+
+    $this->fEventMode = $mode;
+    return $this->fEventMode;
+  }
+
+  /**
+   * Load the fTransitions table from an external resource.
+   *
+   * @param string $url
+   *   Optional: defaults to the name of the class, . ".xml"
+   */
+  public function loadFsm($url = NULL) {
+    $osd = FALSE; // on screen display (debug)
+    if ($osd) {
+      echo "Loading FSM from $url\n";
+    }
+
+    if (!isset($url)) {
+      $url = get_class($this) . ".xml";
+    }
+
+    $fsm = simplexml_load_file($url);
+    $fsmVersion = (string) $fsm['fsm_version'];
+    if ($fsmVersion !== '1.3') {
+      die("Revision $fsmVersion of schema is not supported.\n");
+    }
+
+    $this->idleWork     = ($fsm['idle_work']     == 1);
+    $this->allowActions = ($fsm['allow_actions'] == 1);
+
+    $this->fTransitions = array();
+    $t = &$this->fTransitions;
+
+    // (string) casts in this loop are required: RHS is a SimpleXMLElement
+    foreach ($fsm->state as $state) {
+      $id = (string) $state['id'];
+      if ($osd) {
+        echo "State $id :\n";
+      }
+      $t[$id] = array();
+      switch ($id) {
+        case self::INIT_STATE :
+          if ($osd) {
+            echo "  Initial state\n";
+          }
+          break;
+
+        case self::FINAL_STATE :
+          if ($osd) {
+            echo "  Final state\n";
+          }
+          break;
+      }
+
+      foreach ($state->event as $event) {
+        $name = (string) $event['name'];
+        if ($osd) {
+          echo "  Event $name";
+        }
+
+        if (!isset($event['type'])) {
+          $event['type'] = 'void';
+        }
+
+        $eventType = (string) $event['type'];
+        if ($osd) {
+          echo ", type $eventType\n";
+        }
+
+        foreach ($event as $next) {
+          if ($event['type'] == 'void') {
+            $next['result'] = 'always';
+            $result = null;
+          }
+          else {
+            $result = (string) $next['result'];
+          }
+
+          if (!isset($next['state'])) {
+            $next['state'] = (string) $state['id'];
+          }
+          if ($osd) {
+            echo "    Next(" . $next['result'] . ') = ' . $next['state'];
+            if (isset($next['action'])) {
+              echo " + event " . $next['action'];
+            }
+            echo PHP_EOL;
+          }
+          $t[$id][$name][$result] = array(
+            (string) $next['state'],
+            (string) $next['action']);
+        }
+      }
+    }
+  }
+}

+ 62 - 0
OSInet/Finite_State_Machine/Result.php

@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * Finite state machine skeleton
+ *
+ * @copyright  (c) 2007-2012 Ouest Systèmes Informatiques
+ * @author     Frédéric G. MARAND
+ * @license    Licensed under the CeCILL 2.0
+ * @link       http://wiki.audean.com/fsm/fsm
+ *
+ * @todo       replace setAttribute('id',...) by setIdAttribute when PHP5.2 becomes mandatory
+ */
+
+namespace OSInet\Finite_State_Machine;
+
+/**
+ * needed notably for func_name()
+ */
+require_once('misc.php'); // for func_name()
+
+$erFiniteStateMachine = error_reporting(-1);
+
+/**
+ * This class defines a possible outcome for a given FSM transition.
+ */
+class Result {
+  /**
+   * The return value of the event handler.
+   *
+   * @var mixed
+   */
+  public $fsmReturn;
+
+  /**
+   * The name of the state to which the FSM must change.
+   *
+   * If NULL, do not change the current state.
+   *
+   * @var string
+   */
+  public $fsmState;
+
+  /**
+   * The name of an event to be fired after the state change has been applied.
+   *
+   * @var string
+   */
+  public $fsmAction;
+
+  /**
+   * @param string $state
+   * @param string $action
+   */
+  public function __construct($return = NULL, $state = NULL, $action = NULL) {
+    $this->fsmReturn = $return;
+    $this->fsmState = $state;
+    $this->fsmAction = $action;
+  }
+}
+
+error_reporting($erFiniteStateMachine);
+unset($erFiniteStateMachine);

+ 143 - 162
Fsm_Writer.php → OSInet/Finite_State_Machine/Writer.php

@@ -2,22 +2,20 @@
 /**
  * Finite state machine writer.
  *
- * Keeping the FSM writer (generation) functions out of the root class minimize
+ * Keeping the FSM writer (generation) functions out of the root class minimizes
  * the code volume for FSM readers, which should represent the majority of FSM
  * users.
  *
- * @copyright  (c) 2007 OSI
+ * @copyright  (c) 2007-2012 Ouest Systèmes Informatiques
  * @author     Frédéric G. MARAND
  * @license    Licensed under the CeCILL 2.0
- * @version    CVS: $Id: Fsm_Writer.php,v 1.1 2007-06-10 19:42:35 marand Exp $
  * @link       http://wiki.audean.com/fsm/fsm
- * @since      Not applicable yet
- * @package    fsm
- * @subpackage fsm.core
  */
 
+namespace OSInet\Finite_State_Machine;
+
 /**
- * needed notably for autoload and func_name()
+ * Needed notably for func_name()
  */
 require_once('misc.php');
 
@@ -26,84 +24,79 @@ $erFiniteStateMachine = error_reporting(E_ALL|E_STRICT);
 /**
  * Read/Write concrete FSM class.
  *
- * This (concrete) class extends the default abstract
- * Finite_State_Machine to add write behaviours. It is to be used by
- * applications wishing to write FSM files. Other applications should not
- * use it to avoid code bloat.
- * @package    fsm
- * @subpackage fsm.core
+ * This (concrete) class extends the default abstract Finite_State_Machine to
+ * add write behaviours. It is to be used by applications wishing to write FSM
+ * files. Other applications should not use it to avoid code bloat.
  */
-class Fsm_Writer extends Finite_State_Machine
-  {
-  const VERSION = '$Id: Fsm_Writer.php,v 1.1 2007-06-10 19:42:35 marand Exp $';
+class Writer extends Machine {
 
-  public function __construct()
-    {
-    $this->f_transitions = array();
+  const FORMAT_VERSION = '1.1';
+
+  public function __construct() {
+    $this->fTransitions = array();
     parent::__construct();
-    }
+  }
 
   /**
-   * Return the initial and final states from an abstract FSM graph
+   * Return the initial and final states from an abstract FSM graph.
+   *
+   * @param array $graph
+   *   A concrete FSM derived from the abstract FSM.
    *
-   * @param array $graph Abstract FSM
    * @return array
    */
-  protected function getSpecialStates($graph)
-    {
-    $ret[0] = NULL;
-    $ret[1] = NULL;
-    foreach ($graph['states'] as $state)
-      {
-      if (isset($state['type']) && $state['type'] == 'initial')
-        {
+  protected function getSpecialStates($graph) {
+    $ret = array(NULL, NULL);
+    foreach ($graph['states'] as $state) {
+      if (isset($state['type']) && $state['type'] == 'initial') {
         $ret[0]  = $state['name'];
-        }
-      if (isset($state['type']) && $state['type'] == 'final')
-        {
+      }
+      if (isset($state['type']) && $state['type'] == 'final') {
         $ret[1]  = $state['name'];
-        }
-      if (isset($ret[0]) && isset($ret[1]))
-        {
+      }
+      if (isset($ret[0]) && isset($ret[1])) {
         break;
-        }
       }
-    return $ret;
     }
+    return $ret;
+  }
 
   /**
    * Return the transitions (triggers) available from a given state
    *
-   * @param array $graph Abstract FSM
-   * @param string $id State id in the abstract graph (not id in the XML FSM)
+   * @param array $graph
+   *   A concrete Machine.
+   * @param string $id
+   *   State id in the abstract graph (not id in the XML FSM).
+   *
+   * @return array
    */
-  protected function getTransitionsForState($graph, $id)
-    {
+  protected function getTransitionsForState($graph, $id) {
     $ret = array();
-    foreach ($graph['transitions'] as $txid => $transition)
-      {
+    foreach ($graph['transitions'] as $txid => $transition) {
       // echo "id = $id, txid = $txid from = " . $transition['from'] . PHP_EOL;
-      if ($transition['from'] == $id)
-        {
+      if ($transition['from'] == $id) {
         $ret[] = $txid;
-        }
       }
-    return $ret;
     }
+    return $ret;
+  }
 
   /**
-   * Generate the XML FSM file and optionally PHP stubs file from an abstract
-   * FSM representation as an array of states and transitions
+   * Generate the XML FSM file and optionally PHP stubs file.
+   *
+   * Source is an abstract FSM representation as an array of states/transitions.
    *
    * @param array $graph
    * @param string $prefix
    * @param boolean $php
    * @param boolean $overwrite
+   *
    * @todo take $overwrite and $php into account
+   *
    * @deprecated
    */
-  public function old_save_fsm($graph, $prefix = 'fsm', $php = FALSE, $overwrite = FALSE)
-    {
+  public function oldSaveFsm($graph, $prefix = 'fsm', $php = FALSE, $overwrite = FALSE) {
     echo "This is a dump for FSM $prefix"
       . ', ' . ($php ? 'with' : 'without') . ' PHP generation'
       . ', ' . ($overwrite ? 'with' : 'without') . ' overwrite mode'
@@ -124,12 +117,13 @@ class Fsm_Writer extends Finite_State_Machine
     $fsm->setAttribute('init', $init);
     $fsm->setAttribute('final', $final);
 
-    foreach ($graph['states'] as $id => $state)
-      {
+    foreach ($graph['states'] as $id => $state) {
       $state_node = new DOMElement('state');
       $fsm->appendChild($state_node);
-      $state_node->setAttribute('id', $state['name']); // setIdAttribute not yet implemented in php 5.1
+      // setIdAttribute not yet implemented in php 5.1
+      $state_node->setAttribute('id', $state['name']);
       $transitions = $this->getTransitionsForState($graph, $id);
+
       /**
        * FGM on ne peut pas simplement boucler ainsi: on a dans le graphe
        * abstrait une transition par évènement, alors que dans la FSM les
@@ -137,8 +131,7 @@ class Fsm_Writer extends Finite_State_Machine
        * de boucler ainsi, on crée un "event" pour chaque transition alors
        * qu'il faut créer 1 event et "n" next.
        */
-      foreach ($transitions as $transition_id)
-        {
+      foreach ($transitions as $transition_id) {
         $event_node = new DOMElement('event');
         $state_node->appendChild($event_node);
         $transition = $graph['transitions'][$transition_id];
@@ -147,34 +140,32 @@ class Fsm_Writer extends Finite_State_Machine
          * @todo support other event types
          */
         $event_node->setAttribute('type', 'string');
-        }
       }
+    }
     $doc->save("$prefix.xml");
     print_r($graph['transitions']);
-    }
+  }
 
   /**
-   * Add a new state to the transitions table
+   * Add a new state to the transitions table.
    *
    * @param string $name
+   *
    * @return integer 0 on success, < 0 on failure
    */
-  public function addState($name)
-    {
+  public function addState($name) {
     // echo func_name() . "($name)";
 
-    if (array_key_exists($name, $this->f_transitions))
-      {
+    if (array_key_exists($name, $this->fTransitions)) {
       $ret = -1;
-      }
-    else
-      {
-      $this->f_transitions[$name] = array();
+    }
+    else {
+      $this->fTransitions[$name] = array();
       $ret = 0;
-      }
+    }
     // echo ", ret = $ret\n";
     return $ret;
-    }
+  }
 
   /**
    * Add an event definition (name + optional type of handler) to an existing state
@@ -182,27 +173,28 @@ class Fsm_Writer extends Finite_State_Machine
    * @param string $state
    * @param string $event
    * @param string $type
-   * @return integer 0 on success, < 0 on failure
+   *
+   * @return integer
+   *   - 0 on success
+   *   - < 0 on failure
    */
-  public function addEvent($state, $event, $type = 'string')
-    {
-    if (!array_key_exists($state, $this->f_transitions))
-      {
+  public function addEvent($state, $event, $type = 'string') {
+    if (!array_key_exists($state, $this->fTransitions)) {
       $ret = -1;
-      }
-    elseif (array_key_exists($event, $this->f_transitions[$state]))
-      {
+    }
+    elseif (array_key_exists($event, $this->fTransitions[$state])) {
       $ret = -2;
-      }
-    else // state exists, event doesn't: OK
-      {
-      $this->f_transitions[$state][$event] = array();
-      $this->f_transitions[$state][$event]["#type"] = $type;
+    }
+    // state exists, event doesn't: OK
+    else {
+      $this->fTransitions[$state][$event] = array();
+      $this->fTransitions[$state][$event]["#type"] = $type;
       $ret = 0;
-      }
-    return $ret;
     }
 
+    return $ret;
+  }
+
   /**
    * Add an outcome definition to an existing event:
    * - event handler result
@@ -212,37 +204,35 @@ class Fsm_Writer extends Finite_State_Machine
    *
    * @param name $state
    * @param name $event
-   * @return integer 0 on success, < 0 on failure
+   *
+   * @return integer
+   *   - 0 on success
+   *   - < 0 on failure
    */
   public function addOutcome($state, $event,
     $result     = NULL,  // if null, single possible outcome; only possible on void events
     $next_state = NULL,  // if null, stay on same state
     $action     = NULL)  // if null, do not send an event after transitioning
     {
-    $t = &$this->f_transitions;
-    if (!array_key_exists($state, $t))
-      {
+    $t = &$this->fTransitions;
+    if (!array_key_exists($state, $t)) {
       $ret = -1;
-      }
-    elseif (!array_key_exists($event, $t[$state]))
-      {
+    }
+    elseif (!array_key_exists($event, $t[$state])) {
       $ret = -2;
-      }
-    elseif (($result == NULL) && ($t[$state][$event]['#type'] <> 'void'))
-      {
+    }
+    elseif (($result == NULL) && ($t[$state][$event]['#type'] <> 'void')) {
       $ret = -3; // Undefined, single, results are only available for void events
-      }
-    elseif (($result != NULL) && array_key_exists($result, $t[$state][$event]))
-      {
+    }
+    elseif (($result != NULL) && array_key_exists($result, $t[$state][$event])) {
       $ret = -4; // results must be unique per event
-      }
-    else
-      {
+    }
+    else {
       $t[$state][$event][$result] = array($next_state, $action);
       $ret = 0;
-      }
-    return $ret;
     }
+    return $ret;
+  }
 
   /**
    * Defines the special states in the FSM
@@ -250,40 +240,38 @@ class Fsm_Writer extends Finite_State_Machine
    * @param string $kind Finite_State_Machine::INIT_STATE or ..FINAL_STATE
    * @param string $name
    */
-  public function setSpecialState($kind, $name)
-    {
+  public function setSpecialState($kind, $name) {
     // echo func_name() . "($kind, $name)\n";
-    if (($kind != Finite_State_Machine::INIT_STATE) && ($kind != Finite_State_Machine::FINAL_STATE))
-      {
+    if (($kind != Machine::INIT_STATE) && ($kind != Machine::FINAL_STATE)) {
       $ret = -1; // unknown special state
-      }
-    elseif (!array_key_exists($name, $this->f_transitions))
-      {
+    }
+    elseif (!array_key_exists($name, $this->fTransitions)) {
       $ret = -2; // non existent state declared as special
-      }
-    else
-      {
-      $this->f_transitions["#$kind"] = $name;
+    }
+    else {
+      $this->fTransitions["#$kind"] = $name;
       $ret = 0;
-      }
-    return $ret;
     }
+    return $ret;
+  }
 
   /*
-   * Generate the XML FSM file and optionally PHP stubs file from a valid
-   * instanc of Finite_State_Machine
+   * Generate the XML FSM file and optionally PHP stubs file.
+   *
+   * Source is a valid instance of FSM.
    *
    * @param string $prefix
    * @param boolean $php
    * @param boolean $overwrite
+   *
    * @todo take $overwrite and $php into account
-   * @throws Exception from within _check_transitions
+   *
+   * @throws Exception from within checkTransitions
    */
-  public function save_fsm($prefix = 'fsm', $php = FALSE, $overwrite = FALSE)
-    {
-    $this->_check_transitions();
+  public function saveFsm($prefix = 'fsm', $php = FALSE, $overwrite = FALSE) {
+    $this->checkTransitions();
 
-    $t = &$this->f_transitions;
+    $t = &$this->fTransitions;
     echo "This is a dump for FSM $prefix"
       . ', ' . ($php ? 'with' : 'without') . ' PHP generation'
       . ', ' . ($overwrite ? 'with' : 'without') . ' overwrite mode'
@@ -292,8 +280,8 @@ class Fsm_Writer extends Finite_State_Machine
     $doc = new DOMDocument('1.0', 'utf-8');
     $comment = new DOMComment(' Generated by '
       . basename(__FILE__, '.php')
-      . " version " . Fsm_Writer::VERSION
-      . " on FSM version " . Finite_State_Machine::VERSION
+      . " version " . Writer::FORMAT_VERSION
+      . " on FSM version " . Machine::VERSION
       . " ");
     $doc->appendChild($comment);
     $fsm = new DOMElement('fsm');
@@ -302,54 +290,47 @@ class Fsm_Writer extends Finite_State_Machine
     $fsm->setAttribute('idle_work',     "1");
     $fsm->setAttribute('allow_actions', "1");
 
-    $fsm->setAttribute('init',  $t['#' . Finite_State_Machine::INIT_STATE]);
-    $fsm->setAttribute('final', $t['#' . Finite_State_Machine::FINAL_STATE]);
+    $fsm->setAttribute('init',  $t['#' . Machine::INIT_STATE]);
+    $fsm->setAttribute('final', $t['#' . Machine::FINAL_STATE]);
 
-    foreach ($this->f_transitions as $state_name => $state)
-      {
-      if ($state_name[0] == '#')
-        {
+    foreach ($this->fTransitions as $stateName => $state) {
+      if ($stateName[0] == '#') {
         continue; // ignore special state definitions
-        }
-      $state_node = new DOMElement('state', "\n");
-      $fsm->appendChild($state_node);
-      $state_node->setAttribute('id', $state_name);
-      foreach ($state as $event_name => $event)
-        {
-        $event_node = new DOMElement('event', "\n");
-        $state_node->appendChild($event_node);
-        $event_node->setAttribute('name', $event_name);
-        $event_node->setAttribute('type', $event['#type']);
-        foreach ($event as $outcome_name => $outcome)
-          {
-          if ($outcome_name[0] == '#')
-            {
+      }
+      $stateNode = new DOMElement('state', "\n");
+      $fsm->appendChild($stateNode);
+      $stateNode->setAttribute('id', $stateName);
+      foreach ($state as $eventName => $event) {
+        $eventNode = new DOMElement('event', "\n");
+        $stateNode->appendChild($eventNode);
+        $eventNode->setAttribute('name', $eventName);
+        $eventNode->setAttribute('type', $event['#type']);
+        foreach ($event as $outcomeName => $outcome) {
+          if ($outcomeName[0] == '#') {
             continue; // ignore special outcome definitions (event type)
-            }
-          $outcome_node = new DOMElement('next', "\n");
-          $event_node->appendChild($outcome_node);
+          }
+          $outcomeNode = new DOMElement('next', "\n");
+          $eventNode->appendChild($outcomeNode);
           /**
            * Generated FSMs currently always use the "string" event handler type,
            * meaning they always have outcome results. This will not always be
            * the case, hence this test (think ftp.xml)
            */
-          if (!empty($outcome_name))
-            {
-            $outcome_node->setAttribute('result', $outcome_name);
-            }
+          if (!empty($outcomeName)) {
+            $outcomeNode->setAttribute('result', $outcomeName);
+          }
+
+          if (!empty($outcome[0])) {
+            $outcomeNode->setAttribute('state', $outcome[0]);
+          }
 
-          if (!empty($outcome[0]))
-            {
-            $outcome_node->setAttribute('state', $outcome[0]);
-            }
-          if (!empty($outcome[1]))
-            {
-            $outcome_node->setAttribute('action', $outcome[1]);
-            }
+          if (!empty($outcome[1])) {
+            $outcomeNode->setAttribute('action', $outcome[1]);
           }
         }
       }
+    }
 
     $doc->save("$prefix.xml");
-    }
-  }
+  }
+}

+ 1 - 3
Ftp.xml → ftp.xml

@@ -1,9 +1,7 @@
 <?xml version="1.0" ?>
-<!-- $Id: Ftp.xml,v 1.1 2007-06-03 21:27:57 marand Exp $ -->
 <fsm fsm_version="1.3"
   idle_work="1" allow_actions="1"
-  init="init" final="unsafe"
-  revision="$Revision: 1.1 $">
+  init="init" final="unsafe">
   <state id="init">
     <event name="CheckParameters" type="boolean">
       <next result="true"         state="offline" />

+ 33 - 4
misc.php

@@ -34,11 +34,40 @@ function _debug($msg)
     echo $msg;
   }
 
+/**
+ * Old school autoloader.
+ */
+/*
 function __autoload($name)
   {
   _debug("Autoloading $name\n");
   require_once("$name.php");
   }
+*/
+
+/**
+ * Sample PSR-0 autoloader.
+ *
+ * Do not use as such: it is only placed here to show use of the FSM classes in
+ * a PSR-0 application.
+ *
+ * Straight from the PSR-0 standard.
+ *
+ * @param string $className
+ */
+function psr0_autoload($className) {
+  $className = ltrim($className, '\\');
+  $fileName  = '';
+  $namespace = '';
+  if ($lastNsPos = strripos($className, '\\')) {
+    $namespace = substr($className, 0, $lastNsPos);
+    $className = substr($className, $lastNsPos + 1);
+    $fileName  = str_replace('\\', DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR;
+  }
+  $fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php';
+  //print_r(debug_backtrace());
+  $sts = require $fileName;
+}
 
 function get_temp_dir()
   {
@@ -74,11 +103,11 @@ function load_glade()
  *
  * @return string
  */
-function func_name()
+function func_name($level = 1)
   {
   $trace = debug_backtrace();
-  $func = $trace[1]['function'];
-  if (isset($trace[1]['class']))
-    $func = $trace[1]['class'] . '::' . $func;
+  $func = $trace[$level]['function'];
+  if (isset($trace[$level]['class']))
+    $func = $trace[$level]['class'] . '::' . $func;
   return $func;
   }

+ 71 - 98
u_ftp.php

@@ -23,8 +23,7 @@ require_once('u_fsm.php');
  * - active: a data transfer operation is under way
  * - unsafe: something failed, disconnect can happen out of our control
  */
-class ftp_client extends fsm
-  {
+class ftp_client extends fsm {
   private $f_host;
   private $f_user;
   private $f_pass;
@@ -41,11 +40,10 @@ class ftp_client extends fsm
    * @param string $url
    * @return void
    */
-  public function __construct(string $url = null)
-    {
+  public function __construct(string $url = null) {
     if ($url)
       $this->set_url($url);
-    $this->f_transitions = array(
+    $this->fTransitions = array(
       'init' => array(
         'check_params' => array(
           true     => 'offline',
@@ -120,28 +118,26 @@ class ftp_client extends fsm
          */
       );
     parent::__construct();
-    }
+  }
 
   /**
    * close connection if it hasn't been done, to prevent connection
    * lingering on the server if avoidable
    * @return void
    */
-  public function __destruct()
-    {
+  public function __destruct() {
     if ($this->is_event_allowed('close'))
       $this->f_close();
 
-    if(is_resource($this->f_fp))
-      try
-        {
+    if (is_resource($this->f_fp)) {
+      try {
         fclose($this->f_fp);
-        }
-      catch (Exception $e)
-        {
+      }
+      catch (Exception $e) {
         print_r($e);
-        }
+      }
     }
+  }
 
   /**
    * setter for f_host. Make sur name can be resolved
@@ -149,8 +145,7 @@ class ftp_client extends fsm
    * @param string $host
    * @return ftp_client
    */
-  public function set_host($host = null)
-    {
+  public function set_host($host = null) {
     /**
      * ignore hosts that don't resolve in DNS
      */
@@ -159,9 +154,9 @@ class ftp_client extends fsm
     else
       throw new Exception(func_name() . ": cannot resolve host name \"$host\"");
 
-    $this->apply_event('check_params');
+    $this->applyEvent('check_params');
     return $this;
-    }
+  }
 
   /**
    * setter for f_user
@@ -169,12 +164,11 @@ class ftp_client extends fsm
    * @param string $user
    * @return ftp_client
    */
-  public function set_user($user = 'anonymous')
-    {
+  public function set_user($user = 'anonymous') {
     $this->f_user = $user;
-    $this->apply_event('check_params');
+    $this->applyEvent('check_params');
     return $this;
-    }
+  }
 
   /**
    * setter for f_pass
@@ -182,12 +176,11 @@ class ftp_client extends fsm
    * @param string $pass
    * @return ftp_client
    */
-  public function set_pass($pass = null)
-    {
+  public function set_pass($pass = null) {
     $this->f_pass = $pass;
-    $this->apply_event('check_params');
+    $this->applyEvent('check_params');
     return $this;
-    }
+  }
 
   /**
    * callback is invoked at the end of get, put
@@ -195,28 +188,26 @@ class ftp_client extends fsm
    *
    * @param string $callback
    */
-  public function set_callback($callback)
-    {
+  public function set_callback($callback) {
     if ($callback && !function_exists($callback))
       throw new Exception(func_name() . ": cannot use undefined function $callback as callback");
     else
       $this->f_callback = $callback;
     /**
-     * this setter does not cause a state change, so no call to apply_event
+     * this setter does not cause a state change, so no call to applyEvent
      */
 
     return $this;
-    }
+  }
 
   /**
    * implement change remote directory
    * @return boolean
    */
-  protected function f_chdir()
-    {
+  protected function f_chdir() {
     $ret = ftp_chdir($this->f_conn, $this->f_pwd);
     return $ret;
-    }
+  }
 
   /**
    * change remote directory
@@ -224,12 +215,11 @@ class ftp_client extends fsm
    * @param string $pwd
    * @return boolean
    */
-  public function chdir($pwd = null)
-    {
+  public function chdir($pwd = null) {
     $this->f_pwd = $pwd;
-    $ret = $this->apply_event('chdir');
+    $ret = $this->applyEvent('chdir');
     return $ret;
-    }
+  }
 
   /**
    * setter for f_file
@@ -237,16 +227,15 @@ class ftp_client extends fsm
    * @param string $file
    * @return ftp_client
    */
-  public function set_file($file = null)
-    {
+  public function set_file($file = null) {
     $this->f_file = $file;
     if (!isset($this->f_localfile))
       $this->f_localfile = $file;
 
-    $this->apply_event('check_params');
+    $this->applyEvent('check_params');
 
     return $this;
-    }
+  }
 
   /**
    * setter for f_localfile
@@ -254,22 +243,19 @@ class ftp_client extends fsm
    * @param string $file
    * @return ftp_client
    */
-  public function set_localfile($file = null)
-    {
+  public function set_localfile($file = null) {
     $this->f_localfile = $file;
-    $this->apply_event('check_params');
+    $this->applyEvent('check_params');
 
     return $this;
-    }
-
+  }
 
   /**
    * does the instance have all necessary info for a FTP transfer ?
    *
    * @return boolean
    */
-  protected function f_check_params()
-    {
+  protected function f_check_params() {
     $ret = isset($this->f_host)
       && isset($this->f_user)
       && isset($this->f_pass)
@@ -278,58 +264,52 @@ class ftp_client extends fsm
       ;
     // echo func_name() . ", ret = " . ($ret ? 'TRUE' : 'FALSE') . PHP_EOL;
     return $ret;
-    }
-
+  }
 
   /**
    * implementation of connect
    *
    * @return boolean
    */
-  protected function f_connect()
-    {
+  protected function f_connect() {
     // echo func_name() . "\n";
     $this->f_conn = ftp_connect($this->f_host); // default port, default timeout
     $ret = is_resource($this->f_conn);
     return $ret;
-    }
+  }
 
   /**
    * implementation of close
    * @return boolean
    */
-  protected function f_close()
-    {
+  protected function f_close() {
     // echo func_name() . "\n";
     $ret = ftp_close($this->f_conn);
     return $ret;
-    }
+  }
 
   /**
    * implementation of login
    * @return boolean
    */
-  protected function f_login()
-    {
+  protected function f_login() {
     // echo func_name() . "\n";
     $ret = ftp_login($this->f_conn, $this->f_user, $this->f_pass);
     return $ret;
-    }
+  }
 
   /**
    * implementation of get
    *
    * @return int FTP_FINISHED | FTP_MOREDATA | FTP_FAILED
    */
-  protected function f_get()
-    {
+  protected function f_get() {
     // echo func_name() . "\n";
     $this->f_fp = fopen($this->f_localfile, "wb");
-    if (!is_resource($this->f_fp))
-      {
+    if (!is_resource($this->f_fp)) {
       $ret = FTP_FAILED;
       throw new Exception(func_name() . ": could not create local file $this->f_file");
-      }
+    }
 
     $this->f_goal = ftp_size($this->f_conn, $this->f_file);
 
@@ -338,14 +318,13 @@ class ftp_client extends fsm
       fclose($this->f_fp);
     call_user_func($this->f_callback, $this, 'post');
     return $ret;
-    }
+  }
 
   /**
    * implementation of continue
    * @return int FTP_FINISHED | FTP_MOREDATA | FTP_FAILED
    */
-  protected function f_continue()
-    {
+  protected function f_continue() {
     if ($this->f_callback)
       call_user_func($this->f_callback, $this, 'pre');
     $ret = ftp_nb_continue($this->f_conn);
@@ -354,49 +333,45 @@ class ftp_client extends fsm
     if ($this->f_callback)
       call_user_func($this->f_callback, $this, 'post');
     return $ret;
-    }
+  }
 
   /**
    * interface to connect
    * @return void
    */
-  public function connect()
-    {
+  public function connect() {
     // echo func_name() . "\n";
-    return $this->apply_event('connect');
-    }
+    return $this->applyEvent('connect');
+  }
 
   /**
    * interface to login
    *
    * @return boolean
    */
-  public function login()
-    {
+  public function login() {
     // echo func_name() . "\n";
-    return $this->apply_event('login');
-    }
+    return $this->applyEvent('login');
+  }
 
   /**
    * interface to close
    *
    * @return boolean
    */
-  public function close()
-    {
+  public function close() {
     // echo func_name() . "\n";
-    return $this->apply_event('close');
-    }
+    return $this->applyEvent('close');
+  }
 
   /**
    * get a file using previously defined parameters
    * @return int FTP_FAILED | FTP_MOREDATA | FTP_FINISHED
    */
-  public function get()
-    {
+  public function get() {
     // echo func_name() . "\n";
-    return $this->apply_event('get');
-    }
+    return $this->applyEvent('get');
+  }
 
   /**
    * continue a current transfer
@@ -404,25 +379,23 @@ class ftp_client extends fsm
    * @param string $callback name of function to be called before and after
    * @return int FTP_FINISHED | FTP_MOREDATA | FTP_FAILED
    */
-  public function cont() // continue is a php reserved word
-    {
+  // continue is a php reserved word
+  public function cont() {
     // echo func_name() . "\n";
-    $ret = $this->apply_event('continue');
-    $ret = $ret->fsm_state;
+    $ret = $this->applyEvent('continue');
+    $ret = $ret->fsmState;
     return $ret;
-    }
+  }
 
-  public function get_progress()
-    {
-    if ((!$this->f_state == 'active') || (!is_resource($this->f_fp)))
+  public function get_progress() {
+    if ((!$this->fState == 'active') || (!is_resource($this->f_fp)))
       $ret = 0;
-    else
-      {
+    else {
       $pos = ftell($this->f_fp);
       $ret = $pos / $this->f_goal;
-      }
-    return $ret;
     }
+    return $ret;
+  }
 
   /* missing: put*/
-  }
+}