1 /**
2 Copyright: Copyright (c) 2016-2017, Joakim Brännström. All rights reserved.
3 License: MPL-2
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 This Source Code Form is subject to the terms of the Mozilla Public License,
7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain
8 one at http://mozilla.org/MPL/2.0/.
9 
10 XML utility for generating GraphML content.
11 Contains data structures and serializers.
12 */
13 module dextool.plugin.graphml.backend.xml;
14 
15 import std.format : FormatSpec;
16 import logger = std.experimental.logger;
17 
18 import dextool.hash : makeHash;
19 
20 version (unittest) {
21     import unit_threaded : shouldEqual;
22 }
23 
24 private ulong nextEdgeId()() {
25     static ulong next = 0;
26     return next++;
27 }
28 
29 package ulong nextGraphId()() {
30     static ulong next = 0;
31     return next++;
32 }
33 
34 /// Write the XML header for graphml with needed key definitions.
35 void xmlHeader(RecvT)(ref RecvT recv) {
36     import std.conv : to;
37     import std.format : formattedWrite;
38     import std.range.primitives : put;
39 
40     put(recv, `<?xml version="1.0" encoding="UTF-8"?>` ~ "\n");
41     put(recv, `<graphml` ~ "\n");
42     put(recv, ` xmlns="http://graphml.graphdrawing.org/xmlns"` ~ "\n");
43     put(recv, ` xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"` ~ "\n");
44     put(recv, ` xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns` ~ "\n");
45     put(recv, `   http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd"` ~ "\n");
46     put(recv, ` xmlns:y="http://www.yworks.com/xml/graphml">` ~ "\n");
47 
48     formattedWrite(recv, `<key for="port" id="d%s" yfiles.type="portgraphics"/>` ~ "\n",
49             cast(int) IdT.portgraphics);
50     formattedWrite(recv, `<key for="port" id="d%s" yfiles.type="portgeometry"/>` ~ "\n",
51             cast(int) IdT.portgeometry);
52     formattedWrite(recv, `<key for="port" id="d%s" yfiles.type="portuserdata"/>` ~ "\n",
53             cast(int) IdT.portuserdata);
54     formattedWrite(recv,
55             `<key for="node" attr.name="url" attr.type="string" id="d%s"/>` ~ "\n",
56             cast(int) IdT.url);
57     formattedWrite(recv, `<key for="node" attr.name="description" attr.type="string" id="d%s"/>` ~ "\n",
58             cast(int) IdT.description);
59     formattedWrite(recv, `<key for="node" yfiles.type="nodegraphics" id="d%s"/>` ~ "\n",
60             cast(int) IdT.nodegraphics);
61     formattedWrite(recv, `<key for="graphml" id="d%s" yfiles.type="resources"/>` ~ "\n",
62             cast(int) IdT.graphml);
63     formattedWrite(recv, `<key for="edge" yfiles.type="edgegraphics" id="d%s"/>` ~ "\n",
64             cast(int) IdT.edgegraphics);
65     formattedWrite(recv, `<key for="node" attr.name="position" attr.type="string" id="d%s"/>` ~ "\n",
66             cast(int) IdT.position);
67     formattedWrite(recv, `<key for="node" attr.name="kind" attr.type="string" id="d%s"/>` ~ "\n",
68             cast(int) IdT.kind);
69     formattedWrite(recv, `<key for="node" attr.name="typeAttr" attr.type="string" id="d%s"/>` ~ "\n",
70             cast(int) IdT.typeAttr);
71     formattedWrite(recv, `<key for="node" attr.name="signature" attr.type="string" id="d%s"/>` ~ "\n",
72             cast(int) IdT.signature);
73 
74     formattedWrite(recv, `<graph id="G%s" edgedefault="directed">` ~ "\n", nextGraphId);
75 }
76 
77 @("Should be enum IDs converted to int strings")
78 unittest {
79     // Even though the test seems stupid it exists to ensure that the first and
80     // last ID is as expected. If they mismatch then the header generator is
81     // probably wrong.
82     import std.format : format;
83 
84     format("%s", cast(int) IdT.url).shouldEqual("3");
85     format("%s", cast(int) IdT.signature).shouldEqual("11");
86 }
87 
88 /// Write the xml footer as required by GraphML.
89 void xmlFooter(RecvT)(ref RecvT recv) {
90     import std.range.primitives : put;
91 
92     put(recv, "</graph>\n");
93     put(recv, "</graphml>\n");
94 }
95 
96 package enum ColorKind {
97     none,
98     file,
99     global,
100     globalConst,
101     globalStatic,
102     namespace,
103     func,
104     class_,
105     field,
106     fallback
107 }
108 
109 private struct FillColor {
110     string color1;
111     string color2;
112 
113     void toString(Writer, Char)(scope Writer w, FormatSpec!Char fmt) const {
114         import std.format : formattedWrite;
115 
116         if (color1.length > 0) {
117             formattedWrite(w, `color="#%s"`, color1);
118         }
119 
120         if (color2.length > 0) {
121             formattedWrite(w, ` color2="#%s"`, color2);
122         }
123     }
124 }
125 
126 private FillColor toInternal(ColorKind kind) @safe pure nothrow @nogc {
127     final switch (kind) {
128     case ColorKind.none:
129         return FillColor(null, null);
130     case ColorKind.file:
131         return FillColor("CCFFCC", null);
132     case ColorKind.global:
133         return FillColor("FF33FF", null);
134     case ColorKind.globalConst:
135         return FillColor("FFCCFF", null);
136     case ColorKind.globalStatic:
137         return FillColor("FFD9D2", "FF66FF");
138     case ColorKind.namespace:
139         return FillColor("FFCCCC", null);
140     case ColorKind.func:
141         return FillColor("FF6600", null);
142     case ColorKind.class_:
143         return FillColor("FFCC33", null);
144     case ColorKind.field:
145         return FillColor("FFFF99", null);
146     case ColorKind.fallback:
147         return FillColor("99FF99", null);
148     }
149 }
150 
151 package @safe struct ValidNodeId {
152     import cpptooling.data.type : USRType;
153 
154     size_t payload;
155     alias payload this;
156 
157     this(size_t id) {
158         payload = id;
159     }
160 
161     this(string usr) {
162         payload = makeHash(cast(string) usr);
163         debug logger.tracef("usr:%s -> hash:%s", usr, payload);
164     }
165 
166     this(USRType usr) {
167         this(cast(string) usr);
168     }
169 
170     void toString(Writer, Char)(scope Writer w, FormatSpec!Char fmt) const {
171         import std.format : formatValue;
172 
173         formatValue(w, payload, fmt);
174     }
175 }
176 
177 package enum StereoType {
178     None,
179     Abstract,
180     Interface,
181 }
182 
183 package struct ShapeNode {
184     string label;
185     ColorKind color;
186 
187     private enum baseHeight = 20;
188     private enum baseWidth = 140;
189     private enum graphNode = "ShapeNode";
190 
191     void toString(Writer, Char)(scope Writer w, FormatSpec!Char = "%s") const @safe {
192         import std.format : formattedWrite;
193 
194         formattedWrite(w, `<y:Geometry height="%s" width="%s"/>`, baseHeight, baseWidth);
195         if (color != ColorKind.none) {
196             formattedWrite(w, `<y:Fill %s transparent="false"/>`, color.toInternal);
197         }
198         formattedWrite(w,
199                 `<y:NodeLabel autoSizePolicy="node_size" configuration="CroppingLabel"><![CDATA[%s]]></y:NodeLabel>`,
200                 label);
201     }
202 }
203 
204 package struct UMLClassNode {
205     string label;
206     StereoType stereotype;
207     string[] attributes;
208     string[] methods;
209 
210     private enum baseHeight = 28;
211     private enum graphNode = "UMLClassNode";
212 
213     void toString(Writer)(scope Writer w) const {
214         import std.conv : to;
215         import std.format : formattedWrite;
216         import std.range.primitives : put;
217 
218         formattedWrite(w, `<y:NodeLabel><![CDATA[%s]]></y:NodeLabel>`, label);
219 
220         formattedWrite(w, `<y:Geometry height="%s" width="100.0"/>`,
221                 stereotype == StereoType.None ? baseHeight : baseHeight * 2);
222         put(w, `<y:Fill color="#FFCC00" transparent="false"/>`);
223 
224         formattedWrite(w, `<y:UML clipContent="true" omitDetails="false" stereotype="%s">`,
225                 stereotype == StereoType.None ? "" : stereotype.to!string());
226 
227         if (attributes.length > 0) {
228             put(w, `<y:AttributeLabel>`);
229         }
230         foreach (attr; attributes) {
231             ccdataWrap(w, attr);
232             put(w, "\n");
233         }
234         if (attributes.length > 0) {
235             put(w, `</y:AttributeLabel>`);
236         }
237 
238         if (methods.length > 0) {
239             put(w, `<y:MethodLabel>`);
240         }
241         foreach (meth; methods) {
242             ccdataWrap(w, meth);
243             put(w, "\n");
244         }
245         if (methods.length > 0) {
246             put(w, `</y:MethodLabel>`);
247         }
248 
249         put(w, `</y:UML>`);
250     }
251 }
252 
253 @("Should instantiate all NodeStyles")
254 unittest {
255     import std.array : appender;
256     import std.meta : AliasSeq;
257 
258     auto app = appender!string();
259 
260     foreach (T; AliasSeq!(ShapeNode, UMLClassNode)) {
261         {
262             NodeStyle!T node;
263             node.toString(app, FormatSpec!char("%s"));
264         }
265     }
266 }
267 
268 package auto makeShapeNode(string label, ColorKind color = ColorKind.none) {
269     return NodeStyle!ShapeNode(ShapeNode(label, color));
270 }
271 
272 package auto makeUMLClassNode(string label) {
273     return NodeStyle!UMLClassNode(UMLClassNode(label));
274 }
275 
276 /** Node style in GraphML.
277  *
278  * Intented to carry metadata and formatting besides a generic label.
279  */
280 package struct NodeStyle(PayloadT) {
281     PayloadT payload;
282     alias payload this;
283 
284     void toString(Writer, Char)(scope Writer w, FormatSpec!Char spec) const {
285         import std.format : formattedWrite, formatValue;
286 
287         enum graph_node = PayloadT.graphNode;
288 
289         formattedWrite(w, "<y:%s>", graph_node);
290         formatValue(w, payload, spec);
291         formattedWrite(w, "</y:%s>", graph_node);
292     }
293 }
294 
295 /** Render a FolderNode.
296  *
297  * The node must have, besides an id, the folder attribute:
298  *  yfiles.foldertype="folder"
299  */
300 package @safe struct FolderNode {
301     import std.format : FormatSpec;
302 
303     string label;
304 
305     void toString(Writer, Char)(scope Writer w, FormatSpec!Char fmt) const {
306         import std.range.primitives : put;
307         import std.format : formattedWrite;
308 
309         put(w, `<y:ProxyAutoBoundsNode><y:Realizers active="1">`);
310 
311         // Open
312         put(w, `<y:GroupNode>`);
313         put(w, `<y:Geometry height="50" width="85.0" x="0.0" y="0.0"/>`);
314         put(w, `<y:Fill color="#F5F5F5" transparent="false"/>`);
315         put(w, `<y:BorderStyle color="#000000" type="dashed" width="1.0"/>`);
316         formattedWrite(w, `<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="85.0" x="0.0" y="0.0"><![CDATA[%s]]></y:NodeLabel>`,
317                 label);
318         put(w, `<y:Shape type="roundrectangle"/>`);
319         put(w,
320                 `<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>`);
321         put(w, `<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>`);
322         put(w, `<y:BorderInsets bottom="21" bottomF="21.4609375" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>`);
323         put(w, `</y:GroupNode>`);
324 
325         // Close
326         put(w, `<y:GroupNode>`);
327         put(w, `<y:Geometry height="20.0" width="85.0" x="0.0" y="0.0"/>`);
328         put(w, `<y:Fill color="#F5F5F5" transparent="false"/>`);
329         put(w, `<y:BorderStyle color="#000000" type="dashed" width="1.0"/>`);
330         formattedWrite(w, `<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="85.0" x="0.0" y="0.0"><![CDATA[%s]]></y:NodeLabel>`,
331                 label);
332         put(w, `<y:Shape type="roundrectangle"/>`);
333         put(w,
334                 `<y:State closed="true" closedHeight="20.0" closedWidth="85.0" innerGraphDisplayEnabled="false"/>`);
335         put(w,
336                 `<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>`);
337         put(w,
338                 `<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>`);
339         put(w, `</y:GroupNode>`);
340 
341         put(w, `</y:Realizers></y:ProxyAutoBoundsNode>`);
342     }
343 }
344 
345 package void ccdataWrap(Writer, ARGS...)(scope Writer w, auto ref ARGS args) {
346     import std.range.primitives : put;
347 
348     put(w, `<![CDATA[`);
349     foreach (arg; args) {
350         static if (__traits(hasMember, typeof(arg), "toString")) {
351             arg.toString(w, FormatSpec!char("%s"));
352         } else {
353             put(w, arg);
354         }
355     }
356     put(w, `]]>`);
357 }
358 
359 package void xmlComment(RecvT, CharT)(ref RecvT recv, CharT v) {
360     import std.format : formattedWrite;
361 
362     formattedWrite(recv, "<!-- %s -->\n", v);
363 }
364 
365 package enum IdT {
366     portgraphics,
367     portgeometry,
368     portuserdata,
369     url, /// URL such as a path
370     description,
371     nodegraphics,
372     graphml,
373     edgegraphics,
374     position, /// position in a file
375     kind,
376     typeAttr,
377     signature,
378 }
379 
380 package struct Attr {
381     IdT id;
382 }
383 
384 /// Stream to put attribute data into for complex member methods that handle
385 /// the serialization themself.
386 package alias StreamChar = void delegate(const(char)[]) @safe;
387 
388 /** Serialize a struct into the writer.
389  *
390  * Only those fields and functions tagged with the UDA Attr are serialized into
391  * xml elements.
392  * The "id" as required by GraphML for custom data is derived from Attr.
393  *
394  * Params:
395  *  RecvT = an OutputRange of char
396  *  T = type to analyse for UDA's
397  *  recv = ?
398  *  bundle = ?
399  */
400 package void attrToXml(T, Writer)(ref T bundle, scope Writer recv) {
401     import std.traits : isSomeFunction, stdParameters = Parameters, getUDAs;
402     import std.meta : Alias;
403 
404     static void dataTag(Writer, T)(scope Writer recv, Attr attr, T data) {
405         import std.format : formattedWrite;
406         import std.range.primitives : put;
407 
408         formattedWrite(recv, `<data key="d%s">`, cast(int) attr.id);
409 
410         static if (isSomeFunction!T) {
411             data((const(char)[] buf) @trusted{ put(recv, buf); });
412         } else static if (__traits(hasMember, T, "get")) {
413             // for Nullable etc
414             ccdataWrap(recv, data.get);
415         } else {
416             ccdataWrap(recv, data);
417         }
418 
419         put(recv, "</data>");
420     }
421 
422     // TODO block when trying to stream multiple key's with same id
423 
424     foreach (member_name; __traits(allMembers, T)) {
425         alias memberType = Alias!(__traits(getMember, T, member_name));
426         alias res = getUDAs!(memberType, Attr);
427         // lazy helper for retrieving the compose `bundle.<field>`
428         enum member = "__traits(getMember, bundle, member_name)";
429 
430         static if (res.length == 0) {
431             // ignore those without the UDA Attr
432         } else static if (isSomeFunction!memberType) {
433             // process functions
434             // may only have one parameter and it must accept the delegate StreamChar
435             static if (stdParameters!(memberType).length == 1
436                     && is(stdParameters!(memberType)[0] == StreamChar)) {
437                 dataTag(recv, res[0], &mixin(member));
438             } else {
439                 static assert(0,
440                         "member function tagged with Attr may only take one argument. The argument must be of type "
441                         ~ typeof(StreamChar).stringof ~ " but is of type " ~ stdParameters!(memberType)
442                         .stringof);
443             }
444         } else static if (is(typeof(memberType.init))) {
445             // process basic types
446             dataTag(recv, res[0], mixin(member));
447         }
448     }
449 }
450 
451 @("Should serialize those fields and methods of the struct that has the UDA Attr")
452 unittest {
453     static struct Foo {
454         int ignore;
455         @Attr(IdT.kind) string value;
456         @Attr(IdT.url) void f(StreamChar stream) {
457             stream("f");
458         }
459     }
460 
461     char[] buf;
462     struct Recv {
463         void put(const(char)[] s) {
464             buf ~= s;
465         }
466     }
467 
468     Recv recv;
469     auto s = Foo(1, "value_");
470     attrToXml(s, recv);
471     (cast(string) buf).shouldEqual(
472             `<data key="d9"><![CDATA[value_]]></data><data key="d3">f</data>`);
473 }
474 
475 package enum NodeId;
476 package enum NodeExtra;
477 package enum NodeAttribute;
478 
479 package void nodeToXml(T, Writer)(ref T bundle, scope Writer recv) {
480     import std.format : formattedWrite;
481     import std.range.primitives : put;
482     import std.traits : isSomeFunction, getUDAs;
483     import std.meta : Alias;
484 
485     // lazy helper for retrieving the compose `bundle.<field>`
486     enum member = "__traits(getMember, bundle, member_name)";
487 
488     put(recv, "<node ");
489     scope (success)
490         put(recv, "</node>\n");
491 
492     // Node ID. Can only have one ID.
493     foreach (member_name; __traits(allMembers, T)) {
494         alias memberType = Alias!(__traits(getMember, T, member_name));
495         alias res = getUDAs!(memberType, NodeId);
496 
497         static if (res.length == 0) {
498             // ignore those without the UDA Attr
499         } else static if (isSomeFunction!memberType) {
500             put(recv, `id="`);
501             mixin(member)((const(char)[] buf) @trusted{ put(recv, buf); });
502             put(recv, `"`);
503             break;
504         } else {
505             formattedWrite(recv, `id="%s"`, mixin(member));
506             break;
507         }
508     }
509 
510     // Node xml attribute. Can be more than one.
511     foreach (member_name; __traits(allMembers, T)) {
512         alias memberType = Alias!(__traits(getMember, T, member_name));
513         alias res = getUDAs!(memberType, NodeAttribute);
514 
515         static if (res.length == 0) {
516             // ignore those without the UDA Attr
517         } else static if (isSomeFunction!memberType) {
518             put(recv, " ");
519             mixin(member)((const(char)[] buf) @trusted{ put(recv, buf); });
520         } else {
521             put(recv, " ");
522             formattedWrite(recv, `%s`, mixin(member));
523         }
524     }
525     put(recv, ">");
526     attrToXml(bundle, recv);
527 
528     // Extra node stuff after the attributes
529     foreach (member_name; __traits(allMembers, T)) {
530         alias memberType = Alias!(__traits(getMember, T, member_name));
531         alias res = getUDAs!(memberType, NodeExtra);
532 
533         static if (res.length == 0) {
534             // ignore those without the UDA Attr
535         } else static if (isSomeFunction!memberType) {
536             mixin(member)((const(char)[] buf) @trusted{ put(recv, buf); });
537         } else static if (is(typeof(memberType.init))) {
538             static if (isSomeString!(typeof(memberType))) {
539                 ccdataWrap(recv, mixin(member));
540             } else {
541                 import std.conv : to;
542 
543                 ccdataWrap(recv, mixin(member).to!string());
544             }
545         }
546     }
547 }
548 
549 @("shall serialize a node by the UDA's")
550 unittest {
551     static struct Foo {
552         @NodeId int id;
553         @Attr(IdT.description) string desc;
554 
555         @NodeExtra void extra(StreamChar s) {
556             s("extra");
557         }
558     }
559 
560     char[] buf;
561     struct Recv {
562         void put(const(char)[] s) {
563             buf ~= s;
564         }
565     }
566 
567     Recv recv;
568     auto s = Foo(3, "desc");
569     nodeToXml(s, recv);
570     (cast(string) buf).shouldEqual(
571             `<node id="3"><data key="d4"><![CDATA[desc]]></data>extra</node>
572 `);
573 }
574 
575 package enum EdgeKind {
576     Directed,
577     Generalization
578 }
579 
580 package void xmlEdge(RecvT, SourceT, TargetT)(ref RecvT recv, SourceT src,
581         TargetT target, EdgeKind kind) @safe {
582     import std.conv : to;
583     import std.format : formattedWrite;
584     import std.range.primitives : put;
585 
586     auto src_ = ValidNodeId(src);
587     auto target_ = ValidNodeId(target);
588 
589     debug {
590         import std..string : replace;
591 
592         // printing the raw identifiers to make it easier to debug
593         formattedWrite(recv, `<!-- %s - %s -->`, src.replace("-", "_"), target.replace("-", "_"));
594     }
595 
596     final switch (kind) with (EdgeKind) {
597     case Directed:
598         formattedWrite(recv, `<edge id="e%s" source="%s" target="%s"/>`,
599                 nextEdgeId.to!string, src_, target_);
600         break;
601     case Generalization:
602         formattedWrite(recv, `<edge id="e%s" source="%s" target="%s">`,
603                 nextEdgeId.to!string, src_, target_);
604         formattedWrite(recv,
605                 `<data key="d%s"><y:PolyLineEdge><y:Arrows source="none" target="white_delta"/></y:PolyLineEdge></data>`,
606                 cast(int) IdT.edgegraphics);
607         put(recv, `</edge>`);
608         break;
609     }
610 
611     put(recv, "\n");
612 }