1 <?php
2 /**
3 * Converts a workflow created with yEd Graph Editor (freeware) and saved in graphml format
4 * into an array suitable to be used with the simpleWorkflow extension (sW).<br/>
5 * The conversion is based on the $mapper array, which defines matches between sW attributes
6 * and properties used by yEd.
7 * Following sW values are references using a predefined name :
8 * <ul>
9 * <li>workflow - initial : is the id of the initial node</li>
10 * <li>node - id : id of a given node</li>
11 * <li>node - constraint : PHP expression used as constraint for a node</li>
12 * <li>node - label : text label for a node</li>
13 * <li>node - metadata.* : custom metadata value</li>
14 * <li>edge - task : PHP expression executed when the transition is performed</li>
15 * </ul>
16 * yEd Graph Editor node attributes are referenced in two ways : by their attribute name or by
17 * an xpath expression that applies to the y:ShapeNode element used by yEd to draw each node.
18 * The later is mainly usefule to extract the node label and possibly, some other informations
19 * like for instance the background color or text color used to render each node. The former
20 * is useful to extract built-in yEd attribute and also custom attribute defined by the user.<br/>
21 *
22 */
23 class SWyEdConverter
24 {
25 /**
26 * @var array mapper between SW Workflow attributes and yEd attributes
27 */
28 public $mapper = array(
29 'workflow' => array(
30 'id' => 'Id', // optional
31 'initial' => 'Initial-node-id', // mandatory
32 ),
33 'node' => array(
34 'id' => 'xpath|y:ShapeNode/y:NodeLabel', // mandatory
35 'constraint' => 'Constraint', // optional
36 'label' => 'Label', // optional
37 'metadata.color' => 'xpath|y:ShapeNode/y:NodeLabel/@textColor', // optional
38 'metadata.bgcolor' => 'xpath|y:ShapeNode/y:Fill/@color', // optional
39 ),
40 'edge' => array(
41 'task' => 'Task' // optional
42 )
43 );
44
45 /////////////////////////////////////////////////////////////////////////////////////////////////////
46 // private members
47
48 private $_xml;
49 private $_yedProperties;
50 private $_workflow = array(
51 'workflow' => array(),
52 'node' => array()
53 );
54 /**
55 *
56 * @param string $file name of th graphml (xml) file to process
57 * @return array the workflow definition
58 */
59 public function convert($file)
60 {
61 if(!extension_loaded('domxml')){
62 throw new SWException('extension domxml not loaded : yEd converter requires domxml extension to process');
63 }
64 // reset all
65 $this->_xml = null;
66 $this->_yedProperties = array();
67 $this->_workflow = array(
68 'workflow' => array(),
69 'node' => array()
70 );
71
72 $this->_xml = simplexml_load_file($file);
73 $namespaces = $this->_xml->getNamespaces(true);
74
75 $this->_xml->registerXPathNamespace('y', 'http://www.yworks.com/xml/graphml');
76 $this->_xml->registerXPathNamespace('__empty_ns', $namespaces['']);
77
78 // extract yEd custom attributes IDs ///////////////////////////////////////////////////////////////////
79 //
80 // for instance : <key attr.name="description" attr.type="string" for="node" id="d7"/>
81 // is turned into
82 // array(
83 // 'node' => array(
84 // 'description' => 'd7',
85 // 'nodegraphics' => 'd9',
86 // etc...
87 // )
88 //
89
90 $nlKey = $this->_xml->xpath('/__empty_ns:graphml/__empty_ns:key');
91
92 foreach($nlKey as $ndKey){
93
94 if( $ndKey['attr.name'] != null &&
95 ( $ndKey['for'] == 'node' || $ndKey['for'] == 'edge' || $ndKey['for'] == 'graph'))
96 {
97 $for = (string) $ndKey['for'];
98 $attrName = (string) $ndKey['attr.name'];
99
100 if( ! isset($this->_yedProperties[$for]) ) $this->_yedProperties[$for] = array();
101
102 $this->_yedProperties[$for][$attrName] = (string) $ndKey['id'];
103 }
104 elseif( $ndKey['yfiles.type'] == 'nodegraphics' || $ndKey['yfiles.type'] == 'edgegraphics' )
105 {
106 $for = (string )$ndKey['for'];
107 $fileType = (string) $ndKey['yfiles.type'];
108
109 $this->_yedProperties[$for][$fileType] = (string) $ndKey['id'];
110 }
111 }
112 $nodeGraphicId = $this->_yedProperties['node']['nodegraphics'];
113
114 // extract workflow properties /////////////////////////////////////////////////////
115
116 $nlGraph = $this->_xml->xpath('//__empty_ns:graph');
117 $yedGraphId = (string) $nlGraph[0]['id'];
118
119 foreach($this->mapper['workflow'] as $swAttrName => $yedAttrName)
120 {
121 // echo '<li>Extracting attribute sw:'.$swAttrName.' from yed:'.$yedAttrName.'</li>';
122
123 // creates the XPath that is applied to the yEd XML file in order to retrieve
124 // value for simpleWorkflow attribute $swAttrName (and for the current workflow)
125
126 if( preg_match('/^xpath\|(.*)$/',$yedAttrName,$matches))
127 {
128 $xpath = '//__empty_ns:graph[@id="'.$yedGraphId.'"]/__empty_ns:data[@key="'.$nodeGraphicId.'"]/'.$matches[1];
129 }
130 elseif( isset( $this->_yedProperties['graph'][$yedAttrName]))
131 {
132 $yedDataKey = $this->_yedProperties['graph'][$yedAttrName];
133 $xpath = '//__empty_ns:graph[@id="'.$yedGraphId.'"]/__empty_ns:data[@key="'.$yedDataKey.'"]';
134 }
135 else{
136 continue;
137 }
138
139 // // echo '<li>evaluating xpath = '.$xpath.' : ';
140 $result = $this->_xml->xpath($xpath);
141 if( count($result) == 1 ){
142 // // echo 'value found = <u>'.$result[0].'</u>';
143 $this->_workflow['workflow'][$swAttrName] = trim((string) $result[0]);
144 }
145 else {
146 //$this->_workflow['workflow'][$swAttrName] = null;
147 // // echo 'WARNING : value not found';
148 }
149 // // echo '</li>';
150 }
151 // // echo '</ul>';
152 // // echo '<pre>'.CVarDumper::dumpAsString($this->_workflow['workflow']).'</pre>';
153
154 // extract nodes ///////////////////////////////////////////////////////////////////
155
156 // // echo '<h2>Extracting Nodes</h2>';
157
158 $nlNode = $this->_xml->xpath('//__empty_ns:node');
159 foreach($nlNode as $ndNode)
160 {
161 $yNodeId = (string) $ndNode['id'];
162 $this->_workflow['node'][$yNodeId] = array();
163 // // echo 'yEd node Id = '.$yNodeId.'<br/>';
164
165 foreach($this->mapper['node'] as $swAttrName => $yedAttrName)
166 {
167
168 // // echo 'maping (sw)'.$swAttrName.' <- (yEd)'.$yedAttrName.'<br/>';
169
170 // creates the XPath that is applied to the yEd XML file in order to retrieve
171 // value for simpleWorkflow attribute $swAttrName (and for the current node)
172
173 if( preg_match('/^xpath\|(.*)$/',$yedAttrName,$matches))
174 {
175 $relXpath = $matches[1];
176 $xpath = '//__empty_ns:node[@id="'.$yNodeId.'"]/__empty_ns:data[@key="'.$nodeGraphicId.'"]/'.$relXpath;
177 }
178 elseif( isset( $this->_yedProperties['node'][$yedAttrName]))
179 {
180 $yedDataKey = $this->_yedProperties['node'][$yedAttrName];
181 $xpath = '//__empty_ns:node[@id="'.$yNodeId.'"]/__empty_ns:data[@key="'.$yedDataKey.'"]';
182 }else{
183 continue;
184 }
185
186 // XPath could be created : evaluate it now and store the returned value as a string
187
188 // echo 'evaluating xpath = '.$xpath.'<br/>';
189 $result = $this->_xml->xpath($xpath);
190 if( count($result) == 1 ){
191 // echo 'value found = '.$result[0].'<br/>';
192 $this->_workflow['node'][$yNodeId][$swAttrName] = trim((string) $result[0]);
193 }
194 else {
195 //$this->_workflow['node'][$yNodeId][$swAttrName] = null;
196 // echo 'WARNING : value not found<br/>';
197 }
198 }
199 // echo '<pre>'.CVarDumper::dumpAsString($this->_workflow['node'][$yNodeId]).'</pre>';
200 // echo '<hr/>';
201 }
202
203 // process edges ///////////////////////////////////////////////////////////////////
204
205 // echo '<h2>Extracting Edges</h2>';
206
207 $nlEdge = $this->_xml->xpath('//__empty_ns:edge');
208
209 foreach($nlEdge as $ndEdge)
210 {
211 $yedSource = (string) $ndEdge['source'];
212 $yedTarget = (string) $ndEdge['target'];
213 $yEdgeId = (string) $ndEdge['id'];
214
215 // echo 'processing edge from '.$yedSource.' to '.$yedTarget.'<br/>';
216
217 if( isset($this->_workflow['node'][$yedSource]) && isset($this->_workflow['node'][$yedTarget]))
218 {
219 // echo 'sw nodes found<br/>';
220 $swNodeSource = & $this->_workflow['node'][$yedSource];
221 $swNodeTarget = & $this->_workflow['node'][$yedTarget];
222
223 if(!isset($swNodeSource['transition'])){
224 $swNodeSource['transition'] = array();
225 }
226
227 // is there a task for this transition ? Task is the only attribute
228 // that can be attached to a transition
229
230 $yedAttrName = $this->mapper['edge']['task'];
231
232 if( preg_match('/^xpath\|(.*)$/',$yedAttrName,$matches))
233 {
234 $relXpath = $matches[1];
235 $xpath = '//__empty_ns:edge[@id="'.$yEdgeId.'"]/__empty_ns:data[@key="'.$nodeGraphicId.'"]/'.$relXpath;
236 }
237 elseif( isset( $this->_yedProperties['edge'][$yedAttrName]))
238 {
239 $yedDataKey = $this->_yedProperties['edge'][$yedAttrName];
240 $xpath = '//__empty_ns:edge[@id="'.$yEdgeId.'"]/__empty_ns:data[@key="'.$yedDataKey.'"]';
241 }else{
242 continue;
243 }
244
245 // XPath could be created : evaluate it now and store the returned value as a string
246
247 // echo 'evaluating xpath = '.$xpath.'<br/>';
248 $result = $this->_xml->xpath($xpath);
249 if( count($result) == 1 ){
250 // echo 'value found = '.$result[0].'<br/>';
251 $swNodeSource['transition'][$swNodeTarget['id']] = trim((string) $result[0]);
252 }
253 else {
254 $task = null;
255 // echo 'WARNING : value not found<br/>';
256 $swNodeSource['transition'][] = $swNodeTarget['id'];
257 }
258 }
259 }
260
261 // echo '<h2>Raw Workflow Array</h2>';
262 // echo '<pre>'.CVarDumper::dumpAsString($this->_workflow).'</pre>';
263
264 // normalize workflow array ///////////////////////////////////////////////////////////////////
265
266 // echo '<h2>Normalize Workflow Array</h2>';
267
268 if(isset($this->_workflow['workflow']['initial'])){
269 $this->_workflow['initial'] = $this->_workflow['workflow']['initial'];
270 }
271
272 unset($this->_workflow['workflow']);
273
274 foreach ($this->_workflow['node'] as $key => $node)
275 {
276 $normalizedNode = array();
277 foreach ($node as $nodeAttrName => $nodeAttrValue)
278 {
279 if( preg_match('/^metadata\.(.*)$/',$nodeAttrName,$matches))
280 {
281 if(!isset($normalizedNode['metadata'])){
282 $normalizedNode['metadata'] = array();
283 }
284 // echo 'metadata : '.$mdAttr.' ← '.$nodeAttrValue.'<br/>';
285 $normalizedNode['metadata'][$matches[1]] = $nodeAttrValue;
286 }else {
287 $normalizedNode[$nodeAttrName] = $nodeAttrValue;
288 }
289 }
290 unset($this->_workflow['node'][$key]);
291 $this->_workflow['node'][] = $normalizedNode;
292 }
293 return $this->_workflow;
294 }
295 }