Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
98 / 98
GraphMLReader
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
6 / 6
36
100.00% covered (success)
100.00%
98 / 98
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 parseGraphMLFile
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
 parseDomToGraph
100.00% covered (success)
100.00%
1 / 1
27
100.00% covered (success)
100.00%
73 / 73
 buildStateMachine
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
17 / 17
 str2key
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 getGraph
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
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;
    }
}