Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
100.00% |
1 / 1 |
|
100.00% |
6 / 6 |
CRAP | |
100.00% |
98 / 98 |
GraphMLReader | |
100.00% |
1 / 1 |
|
100.00% |
6 / 6 |
36 | |
100.00% |
98 / 98 |
__construct | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
parseGraphMLFile | |
100.00% |
1 / 1 |
2 | |
100.00% |
4 / 4 |
|||
parseDomToGraph | |
100.00% |
1 / 1 |
27 | |
100.00% |
73 / 73 |
|||
buildStateMachine | |
100.00% |
1 / 1 |
4 | |
100.00% |
17 / 17 |
|||
str2key | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
getGraph | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
<?php | |
/* | |
* Copyright (c) 2016, Josef Kufner <josef@kufner.cz> | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
* | |
*/ | |
namespace Smalldb\StateMachine\GraphMLExtension; | |
use DOMDocument; | |
use DOMXpath; | |
use Smalldb\StateMachine\Definition\Builder\StateMachineDefinitionBuilder; | |
use Smalldb\Graph\Graph; | |
use Smalldb\Graph\MissingElementException; | |
use Smalldb\StateMachine\StyleExtension\Definition\StyleExtensionPlaceholder; | |
/** | |
* GraphML reader | |
* Load state machine definition from GraphML created by yEd graph editor. | |
* Options: | |
* - `group`: ID of the subdiagram to use. If null, the whole diagram is | |
* used. | |
* | |
* @see http://www.yworks.com/en/products_yed_about.html | |
*/ | |
class GraphMLReader | |
{ | |
private StateMachineDefinitionBuilder $builder; | |
private ?Graph $graph = null; | |
public function __construct(StateMachineDefinitionBuilder $builder) | |
{ | |
$this->builder = $builder; | |
} | |
public function parseGraphMLFile(string $fileName, ?string $graphml_group_name = null) | |
{ | |
// Load GraphML into DOM | |
$dom = new DOMDocument; | |
$dom->load($fileName); | |
$this->graph = $this->parseDomToGraph($dom, $graphml_group_name); | |
return $this->buildStateMachine($this->graph); | |
} | |
private function parseDomToGraph(DOMDocument $dom, ?string $graphml_group_name = null): Graph | |
{ | |
// Prepare the Graph | |
$graph = new Graph(); | |
$keys = array(); | |
// Prepare XPath query engine | |
$xpath = new DOMXpath($dom); | |
$xpath->registerNameSpace('g', 'http://graphml.graphdrawing.org/xmlns'); | |
// Find group node | |
if ($graphml_group_name) { | |
$root_graph = null; | |
foreach($xpath->query('//g:graph') as $el) { | |
foreach($xpath->query('../g:data/*/*/y:GroupNode/y:NodeLabel', $el) as $label_el) { | |
$label = trim($label_el->textContent); | |
if ($label == $graphml_group_name) { | |
$root_graph = $el; | |
break 2; | |
} | |
} | |
} | |
} else { | |
$root_graph = $xpath->query('/g:graphml/g:graph')->item(0); | |
} | |
if ($root_graph == null) { | |
throw new GraphMLException('Graph node not found: ' . $graphml_group_name); | |
} | |
// Load keys | |
foreach($xpath->query('./g:key[@attr.name][@id]') as $el) { | |
$id = $el->attributes->getNamedItem('id')->value; | |
$name = $el->attributes->getNamedItem('attr.name')->value; | |
$keys[$id] = $name; | |
} | |
// Load graph properties | |
foreach($xpath->query('./g:data[@key]', $root_graph) as $data_el) { | |
$k = $data_el->attributes->getNamedItem('key')->value; | |
if (isset($keys[$k])) { | |
if ($keys[$k] == 'Properties') { | |
// Special handling of machine properties | |
// (XML property named "Properties" of the root graph) | |
$properties = array(); | |
foreach ($xpath->query('./property[@name]', $data_el) as $property_el) { | |
$property_name = $property_el->attributes->getNamedItem('name')->value; | |
foreach ($property_el->attributes as $property_attr_name => $property_attr) { | |
$properties[$property_name][$property_attr_name] = $property_attr->value; | |
} | |
} | |
$graph->setAttr('properties', $properties); | |
} else { | |
$graph->setAttr($this->str2key($keys[$k]), trim($data_el->textContent)); | |
} | |
} | |
} | |
// Load nodes | |
foreach($xpath->query('.//g:node[@id]', $root_graph) as $el) { | |
$id = $el->attributes->getNamedItem('id')->value; | |
$node = $graph->createNode($id); | |
foreach($xpath->query('.//g:data[@key]', $el) as $data_el) { | |
$k = $data_el->attributes->getNamedItem('key')->value; | |
if (isset($keys[$k])) { | |
$node->setAttr($this->str2key($keys[$k]), $data_el->textContent); | |
} | |
} | |
$label = $xpath->query('.//y:NodeLabel', $el)->item(0)->textContent; | |
if ($label !== null) { | |
$node->setAttr('label', trim($label)); | |
} | |
$color = $xpath->query('.//y:Fill', $el)->item(0)->attributes->getNamedItem('color')->value; | |
if ($color !== null) { | |
$node->setAttr('color', trim($color)); | |
} | |
} | |
// Load edges | |
foreach($xpath->query('//g:graph/g:edge[@id][@source][@target]') as $el) { | |
$id = $el->attributes->getNamedItem('id')->value; | |
$source = $el->attributes->getNamedItem('source')->value; | |
$target = $el->attributes->getNamedItem('target')->value; | |
try { | |
$sourceNode = $graph->getNode($source); | |
$targetNode = $graph->getNode($target); | |
} | |
catch (MissingElementException $ex) { | |
continue; | |
} | |
$edge = $graph->createEdge($id, $sourceNode, $targetNode); | |
foreach($xpath->query('.//g:data[@key]', $el) as $data_el) { | |
$k = $data_el->attributes->getNamedItem('key')->value; | |
if (isset($keys[$k])) { | |
$edge->setAttr($this->str2key($keys[$k]), $data_el->textContent); | |
} | |
} | |
$label_query_result = $xpath->query('.//y:EdgeLabel', $el)->item(0); | |
if (!$label_query_result) { | |
throw new GraphMLException(sprintf('Missing edge label. Edge: %s -> %s', | |
$sourceNode->getAttr('label', $source), | |
$targetNode->getAttr('label', $target))); | |
} | |
$label = $label_query_result->textContent; | |
if ($label === null || ($label = trim($label)) === "") { | |
throw new GraphMLException(sprintf('Empty edge label. Edge: %s -> %s', | |
$sourceNode->getAttr('label', $source), | |
$targetNode->getAttr('label', $target))); | |
} else { | |
$edge->setAttr('label', $label); | |
} | |
$color_query_result = $xpath->query('.//y:LineStyle', $el)->item(0); | |
if ($color_query_result) { | |
$color = $color_query_result->attributes->getNamedItem('color')->value; | |
if ($color !== null) { | |
$edge->setAttr('color', trim($color)); | |
} | |
} | |
} | |
return $graph; | |
} | |
private function buildStateMachine(Graph $graph): StateMachineDefinitionBuilder | |
{ | |
// TODO: Attach the original graph to the definition | |
// TODO: Process graph attrs. | |
// Create states | |
foreach ($graph->getNodes() as $n) { | |
$stateName = (string) $n->getAttr('label'); | |
if ($stateName !== '') { | |
$state = $this->builder->addState($stateName); | |
/** @var StyleExtensionPlaceholder $ext */ | |
$ext = $state->getExtensionPlaceholder(StyleExtensionPlaceholder::class); | |
$ext->color = $n->getAttr('color'); | |
// TODO: Process node attrs. | |
} | |
} | |
// Store actions and transitions | |
$transitions = []; | |
foreach ($graph->getEdges() as $e) { | |
$label = (string) $e->getAttr('label'); | |
$sourceStateName = (string) $e->getStart()->getAttr('label'); | |
$targetStateName = (string) $e->getEnd()->getAttr('label'); | |
$transition = $this->builder->getTransition($label, $sourceStateName); | |
$transition->targetStates[] = $targetStateName; | |
/** @var StyleExtensionPlaceholder $ext */ | |
$ext = $transition->getExtensionPlaceholder(StyleExtensionPlaceholder::class); | |
$ext->color = $e->getAttr('color'); | |
// TODO: Process edge attrs. | |
} | |
// Sort stuff to keep them in order when file is modified | |
$this->builder->sortPlaceholders(); | |
return $this->builder; | |
} | |
/** | |
* Convert some nice property name to key suitable for JSON file. | |
*/ | |
private function str2key($str) | |
{ | |
return strtr(mb_strtolower($str), ' ', '_'); | |
} | |
public function getGraph(): Graph | |
{ | |
return $this->graph; | |
} | |
} | |