diff --git a/src/main/java/swiss/sib/rdf/sparql/examples/SparqlInRdfToMermaid.java b/src/main/java/swiss/sib/rdf/sparql/examples/SparqlInRdfToMermaid.java index 89eecfd2d..168c919f1 100644 --- a/src/main/java/swiss/sib/rdf/sparql/examples/SparqlInRdfToMermaid.java +++ b/src/main/java/swiss/sib/rdf/sparql/examples/SparqlInRdfToMermaid.java @@ -13,225 +13,42 @@ import java.util.Set; import java.util.TreeMap; import java.util.stream.Collectors; -import java.util.stream.IntStream; import java.util.stream.Stream; -import org.eclipse.rdf4j.model.IRI; -import org.eclipse.rdf4j.model.Literal; import org.eclipse.rdf4j.model.Model; import org.eclipse.rdf4j.model.Resource; import org.eclipse.rdf4j.model.Statement; import org.eclipse.rdf4j.model.Value; -import org.eclipse.rdf4j.model.base.CoreDatatype; import org.eclipse.rdf4j.model.vocabulary.RDF; import org.eclipse.rdf4j.model.vocabulary.SHACL; import org.eclipse.rdf4j.query.MalformedQueryException; -import org.eclipse.rdf4j.query.algebra.LeftJoin; -import org.eclipse.rdf4j.query.algebra.Service; -import org.eclipse.rdf4j.query.algebra.StatementPattern; import org.eclipse.rdf4j.query.algebra.TupleExpr; -import org.eclipse.rdf4j.query.algebra.Var; -import org.eclipse.rdf4j.query.algebra.helpers.AbstractQueryModelVisitor; import org.eclipse.rdf4j.query.parser.ParsedQuery; import org.eclipse.rdf4j.query.parser.QueryParser; import org.eclipse.rdf4j.query.parser.sparql.SPARQLParserFactory; - - +import swiss.sib.rdf.sparql.examples.mermaid.FindWhichConstantsAreNotOnlyUsedAsPredicates; +import swiss.sib.rdf.sparql.examples.mermaid.NameVariablesAndConstants; +import swiss.sib.rdf.sparql.examples.mermaid.Render; import swiss.sib.rdf.sparql.examples.vocabularies.SIB; import swiss.sib.rdf.sparql.examples.vocabularies.SchemaDotOrg; public class SparqlInRdfToMermaid { - private static final class Render extends AbstractQueryModelVisitor { - private final Map variableKeys; - private final Map iriPrefixes; - private final Map constantKeys; - private final Set usedAsNode; - private final Map anonymousKeys; - private final List rq; - private int indent = 2; - private boolean optional; - - private Render(Map variableKeys, Map iriPrefixes, - Map constantKeys, Set usedAsNode, Map anonymousKeys, - List rq) { - this.variableKeys = variableKeys; - this.iriPrefixes = iriPrefixes; - this.constantKeys = constantKeys; - this.usedAsNode = usedAsNode; - this.anonymousKeys = anonymousKeys; - this.rq = rq; - } - @Override - public void meet(StatementPattern node) throws RuntimeException { - String subj = asString(node.getSubjectVar()); - String obj = asString(node.getObjectVar()); - if (node.getPredicateVar().isConstant() && !usedAsNode.contains(node.getPredicateVar().getValue())) { - String pred = prefix(node.getPredicateVar().getValue(), iriPrefixes); - rq.add(print(subj, obj, pred)); - } else { - String pred = asString(node.getPredicateVar()); - rq.add(indent() + subj + " -->" + pred + "--> " - + obj); - } - super.meet(node); - } - - private String print(String subj, String obj, String pred) { - String arrB = "--"; - String arrE = "-->"; - if (optional) { - arrB = "-."; - arrE = ".->"; - optional=false; // Afterwards they should be solid again - } - return indent() + subj + arrB + pred + arrE +" " - + obj; - } - - @Override - public void meet(LeftJoin s) throws RuntimeException { - s.getLeftArg().visit(this); - optional=true; - rq.add(indent()+"%%optional"); - indent+=2; - s.getRightArg().visit(this); - indent-=2; - } - - @Override - public void meet(Service s) throws RuntimeException { - rq.add(indent()+"subgraph \"" + s.getServiceRef().getValue().stringValue()+'"'); - indent+=2; - rq.add(indent()+"direction LR"); - super.meet(s); - indent-=2; - rq.add(indent()+"end"); - } - - private String indent() { - return IntStream.range(0, indent).mapToObj(i -> " ").collect(Collectors.joining()); - } - - private String asString(Var v) { - if (v.isAnonymous() && !v.isConstant()) { - return anonymousKeys.get(v.getName()); - } else if (v.isConstant()) { - return constantKeys.get(v.getValue()); - } else { - return variableKeys.get(v.getName()); - } - } - } - - private static final class FindWhichConstantsAreNotOnlyUsedAsPredicates - extends AbstractQueryModelVisitor { - private final Set usedAsNode; - - private FindWhichConstantsAreNotOnlyUsedAsPredicates(Set usedAsNode) { - this.usedAsNode = usedAsNode; - } - - @Override - public void meet(StatementPattern node) throws RuntimeException { - if (node.getPredicateVar().isConstant()) { - markAsUsed(node.getSubjectVar(), node.getObjectVar()); - } else { - markAsUsed(node.getSubjectVar(), node.getPredicateVar(), node.getObjectVar()); - } - super.meet(node); - } - - private void markAsUsed(Var... vars) { - for (Var v : vars) { - usedAsNode.add(v.getValue()); - } - } - } - - public static final class NameVariablesAndConstants extends AbstractQueryModelVisitor { - private final Map constantKeys; - private final Map variableKeys; - private final Map anonymousKeys; - - public NameVariablesAndConstants(Map constantKeys, Map variableKeys, - Map anonymousKeys) { - this.constantKeys = constantKeys; - this.variableKeys = variableKeys; - this.anonymousKeys = anonymousKeys; - } - - @Override - public void meet(Var node) throws RuntimeException { - super.meet(node); - if (node.isAnonymous() && !node.isConstant()) { - if (!anonymousKeys.containsKey(node.getName())) { - String nextId = "a" + (anonymousKeys.size() + 1); - anonymousKeys.put(node.getName(), nextId); - } - } else if (!node.isConstant() && !variableKeys.containsKey(node.getName())) { - String nextId = "v" + (variableKeys.size() + 1); - variableKeys.put(node.getName(), nextId); - - } else if (node.isConstant() && !constantKeys.containsKey(node.getValue())) { - String nextId = "c" + (constantKeys.size() + 1); - constantKeys.put(node.getValue(), nextId); - } - } - - } - - private static String prefix(String stringValue, Map iriPrefixes) { - for (Map.Entry en : iriPrefixes.entrySet()) { - if (stringValue.startsWith(en.getKey())) { - return '"' + en.getValue() + stringValue.substring(en.getKey().length()) + '"'; - } - } - return stringValue; - } - - private static String prefix(Value v, Map iriPrefixes) { - if (v instanceof IRI i) - return prefix(i, iriPrefixes); - else if (v instanceof Literal l) - return prefix(l, iriPrefixes); - else - return v.stringValue(); - } - - private static String prefix(IRI stringValue, Map iriPrefixes) { - return prefix(stringValue.stringValue(), iriPrefixes); - } - - private static String prefix(Literal l, Map iriPrefixes) { - CoreDatatype cd = l.getCoreDatatype(); - if (cd.isXSDDatatype()) { - if (CoreDatatype.XSD.STRING.equals(cd)) - return '"' + l.stringValue() + '"'; - else if (cd.isXSDDatatype()) - return '"' + l.stringValue() + "\"^^xsd:" + cd.getIri().getLocalName(); - else if (cd.isRDFDatatype()) - return '"' + l.stringValue() + "\"^^rdf:" + cd.getIri().getLocalName(); - else if (cd.isGEODatatype()) - return '"' + l.stringValue() + "\"^^geo:" + cd.getIri().getLocalName(); - } - return '"' + l.stringValue() + "\"^^<" + l.getDatatype() + ">"; - } /** - * {@link https://grlc.io/} + * {@link https://mermaid.js.org} * * @param ex the model containing all prefixes and query - * @return a rq formatted list of strings that should be concatenated later. + * @return a a mermaid string */ public static String asMermaid(Model ex) { List rq = new ArrayList<>(); + rq.add("graph TD"); streamOf(ex, null, RDF.TYPE, SHACL.SPARQL_EXECUTABLE).map(Statement::getSubject).distinct().forEach(s -> { - rq.add("graph TD"); Stream.of(SHACL.ASK, SHACL.SELECT, SHACL.CONSTRUCT, SIB.DESCRIBE).flatMap(qt -> streamOf(ex, s, qt, null)) - .forEach(q -> addPrefixes(q, ex, rq)); + .forEach(q -> draw(q, ex, rq)); }); return rq.stream().collect(Collectors.joining("\n")); } @@ -241,7 +58,7 @@ public static String asMermaid(Model ex) { * * @param rq **/ - public static void addPrefixes(Statement queryId, Model ex, List rq) { + public static void draw(Statement queryId, Model ex, List rq) { String query = queryId.getObject().stringValue(); String base = streamOf(ex, queryId.getSubject(), SchemaDotOrg.TARGET, null).map(Statement::getObject) @@ -266,12 +83,7 @@ public static void addPrefixes(Statement queryId, Model ex, List rq) { tq.visit(new NameVariablesAndConstants(constantKeys, variableKeys, anonymousKeys)); tq.visit(new FindWhichConstantsAreNotOnlyUsedAsPredicates(usedAsNode)); - addWithLeadingWhiteSpace(variableKeys.entrySet().stream().sorted(Comparator.comparing(Map.Entry::getKey)) - .map(en -> en.getValue() + "(\"?" + en.getKey() + "\")"), rq); - addWithLeadingWhiteSpace(anonymousKeys.entrySet().stream().sorted(Comparator.comparing(Map.Entry::getKey)) - .map(en -> en.getValue() + "((\"_:" + en.getValue() + "\"))"), rq); - addWithLeadingWhiteSpace(constantKeys.entrySet().stream().filter(en -> usedAsNode.contains(en.getKey())) - .map(en -> en.getValue() + "([" + prefix(en.getKey(), iriPrefixes) + "])"), rq); + tq.visit( new Render(variableKeys, iriPrefixes, constantKeys, usedAsNode, anonymousKeys, rq)); @@ -281,9 +93,7 @@ public static void addPrefixes(Statement queryId, Model ex, List rq) { } } - private static void addWithLeadingWhiteSpace(Stream map, List rq) { - map.map(s -> " " + s).forEach(rq::add); - } + private static void gatherCompleteQuery(Model ex, String query, Iterator iterator, StringBuilder prefixes, Map iriPrefixes) { diff --git a/src/main/java/swiss/sib/rdf/sparql/examples/mermaid/FindWhichConstantsAreNotOnlyUsedAsPredicates.java b/src/main/java/swiss/sib/rdf/sparql/examples/mermaid/FindWhichConstantsAreNotOnlyUsedAsPredicates.java new file mode 100644 index 000000000..cf8850819 --- /dev/null +++ b/src/main/java/swiss/sib/rdf/sparql/examples/mermaid/FindWhichConstantsAreNotOnlyUsedAsPredicates.java @@ -0,0 +1,33 @@ +package swiss.sib.rdf.sparql.examples.mermaid; + +import java.util.Set; + +import org.eclipse.rdf4j.model.Value; +import org.eclipse.rdf4j.query.algebra.StatementPattern; +import org.eclipse.rdf4j.query.algebra.Var; +import org.eclipse.rdf4j.query.algebra.helpers.AbstractQueryModelVisitor; + +public final class FindWhichConstantsAreNotOnlyUsedAsPredicates + extends AbstractQueryModelVisitor { + private final Set usedAsNode; + + public FindWhichConstantsAreNotOnlyUsedAsPredicates(Set usedAsNode) { + this.usedAsNode = usedAsNode; + } + + @Override + public void meet(StatementPattern node) throws RuntimeException { + if (node.getPredicateVar().isConstant()) { + markAsUsed(node.getSubjectVar(), node.getObjectVar()); + } else { + markAsUsed(node.getSubjectVar(), node.getPredicateVar(), node.getObjectVar()); + } + super.meet(node); + } + + private void markAsUsed(Var... vars) { + for (Var v : vars) { + usedAsNode.add(v.getValue()); + } + } +} \ No newline at end of file diff --git a/src/main/java/swiss/sib/rdf/sparql/examples/mermaid/NameVariablesAndConstants.java b/src/main/java/swiss/sib/rdf/sparql/examples/mermaid/NameVariablesAndConstants.java new file mode 100644 index 000000000..9de59da49 --- /dev/null +++ b/src/main/java/swiss/sib/rdf/sparql/examples/mermaid/NameVariablesAndConstants.java @@ -0,0 +1,39 @@ +package swiss.sib.rdf.sparql.examples.mermaid; + +import java.util.Map; + +import org.eclipse.rdf4j.model.Value; +import org.eclipse.rdf4j.query.algebra.Var; +import org.eclipse.rdf4j.query.algebra.helpers.AbstractQueryModelVisitor; + +public final class NameVariablesAndConstants extends AbstractQueryModelVisitor { + private final Map constantKeys; + private final Map variableKeys; + private final Map anonymousKeys; + + public NameVariablesAndConstants(Map constantKeys, Map variableKeys, + Map anonymousKeys) { + this.constantKeys = constantKeys; + this.variableKeys = variableKeys; + this.anonymousKeys = anonymousKeys; + } + + @Override + public void meet(Var node) throws RuntimeException { + super.meet(node); + if (node.isAnonymous() && !node.isConstant()) { + if (!anonymousKeys.containsKey(node.getName())) { + String nextId = "a" + (anonymousKeys.size() + 1); + anonymousKeys.put(node.getName(), nextId); + } + } else if (!node.isConstant() && !variableKeys.containsKey(node.getName())) { + String nextId = "v" + (variableKeys.size() + 1); + variableKeys.put(node.getName(), nextId); + + } else if (node.isConstant() && !constantKeys.containsKey(node.getValue())) { + String nextId = "c" + (constantKeys.size() + 1); + constantKeys.put(node.getValue(), nextId); + } + } + +} \ No newline at end of file diff --git a/src/main/java/swiss/sib/rdf/sparql/examples/mermaid/Render.java b/src/main/java/swiss/sib/rdf/sparql/examples/mermaid/Render.java new file mode 100644 index 000000000..acc94ebd8 --- /dev/null +++ b/src/main/java/swiss/sib/rdf/sparql/examples/mermaid/Render.java @@ -0,0 +1,261 @@ +package swiss.sib.rdf.sparql.examples.mermaid; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import org.eclipse.rdf4j.model.IRI; +import org.eclipse.rdf4j.model.Literal; +import org.eclipse.rdf4j.model.Value; +import org.eclipse.rdf4j.model.base.CoreDatatype; +import org.eclipse.rdf4j.model.vocabulary.RDF; +import org.eclipse.rdf4j.query.algebra.Compare; +import org.eclipse.rdf4j.query.algebra.Exists; +import org.eclipse.rdf4j.query.algebra.Filter; +import org.eclipse.rdf4j.query.algebra.LeftJoin; +import org.eclipse.rdf4j.query.algebra.Not; +import org.eclipse.rdf4j.query.algebra.SameTerm; +import org.eclipse.rdf4j.query.algebra.Service; +import org.eclipse.rdf4j.query.algebra.StatementPattern; +import org.eclipse.rdf4j.query.algebra.Union; +import org.eclipse.rdf4j.query.algebra.Var; +import org.eclipse.rdf4j.query.algebra.helpers.AbstractQueryModelVisitor; + +public final class Render extends AbstractQueryModelVisitor { + private final class FindNodesUsedInFilter extends AbstractQueryModelVisitor { + private final String filterId; + + private FindNodesUsedInFilter(String filterId) { + this.filterId = filterId; + } + + @Override + public void meet(Var node) throws RuntimeException { + rq.add(indent() + filterId + " .-> " + asString(node)); + } + + @Override + public void meet(SameTerm node) throws RuntimeException { + rq.add(indent() + filterId + "{{ sameTerm }}"); + super.meet(node); + } + + @Override + public void meet(Not node) throws RuntimeException { + rq.add(indent() + filterId + "{{ Not }}"); + super.meet(node); + } + + @Override + public void meet(Exists node) throws RuntimeException { + rq.add(indent() + "subgraph " + filterId + "{{ Exists }}"); + Map constantKeys = new HashMap<>(); + Map variableKeys = new HashMap<>(); + Map anonymousKeys = new HashMap<>(); + Set usedAsNode = new HashSet<>(); + + node.visit(new NameVariablesAndConstants(constantKeys, variableKeys, anonymousKeys)); + node.visit(new FindWhichConstantsAreNotOnlyUsedAsPredicates(usedAsNode)); + node.visit(new Render(variableKeys, iriPrefixes, constantKeys, usedAsNode, anonymousKeys, rq)); + + } + + @Override + public void meet(Compare node) throws RuntimeException { + switch(node.getOperator()) { + case EQ -> + rq.add(indent() + filterId + "{{ = }}"); + case NE -> + rq.add(indent() + filterId + "{{ != }}"); + case LT -> + rq.add(indent() + filterId + "{{ < }}"); + case GT -> + rq.add(indent() + filterId + "{{ > }}"); + case LE -> + rq.add(indent() + filterId + "{{ <= }}"); + case GE -> + rq.add(indent() + filterId + "{{ >= }}"); + } + super.meet(node); + } + } + + private final Map variableKeys; + private final Map iriPrefixes; + private final Map constantKeys; + private final Set usedAsNode; + private final Map serviceKeys = new HashMap<>(); + private final Map anonymousKeys; + private final List rq; + private int indent = 2; + private int optionalCount = 0; + private int unionCount = 0; + private boolean optional; + private int filterCount; + + public Render(Map variableKeys, Map iriPrefixes, Map constantKeys, + Set usedAsNode, Map anonymousKeys, List rq) { + this.variableKeys = variableKeys; + this.iriPrefixes = iriPrefixes; + this.constantKeys = constantKeys; + this.usedAsNode = usedAsNode; + this.anonymousKeys = anonymousKeys; + this.rq = rq; + addWithLeadingWhiteSpace(variableKeys.entrySet().stream().sorted(Comparator.comparing(Map.Entry::getKey)) + .map(en -> en.getValue() + "(\"?" + en.getKey() + "\")"), rq); + addWithLeadingWhiteSpace(anonymousKeys.entrySet().stream().sorted(Comparator.comparing(Map.Entry::getKey)) + .map(en -> en.getValue() + "((\"_:" + en.getValue() + "\"))"), rq); + addWithLeadingWhiteSpace(constantKeys.entrySet().stream().filter(en -> usedAsNode.contains(en.getKey())) + .map(en -> en.getValue() + "([" + prefix(en.getKey(), iriPrefixes) + "])"), rq); + addStyles(rq); + } + + private void addStyles(List rq2) { +// rq2.add("classdef service fill:#f96"); + } + + private static void addWithLeadingWhiteSpace(Stream map, List rq) { + map.map(s -> " " + s).forEach(rq::add); + } + + @Override + public void meet(StatementPattern node) throws RuntimeException { + String subj = asString(node.getSubjectVar()); + String obj = asString(node.getObjectVar()); + if (node.getPredicateVar().isConstant() && !usedAsNode.contains(node.getPredicateVar().getValue())) { + String pred = prefix(node.getPredicateVar().getValue(), iriPrefixes); + rq.add(print(subj, obj, pred)); + } else { + String pred = asString(node.getPredicateVar()); + rq.add(indent() + subj + " -->" + pred + "--> " + obj); + } + super.meet(node); + } + + private String print(String subj, String obj, String pred) { + String arrB = "--"; + String arrE = "-->"; + if (optional) { + arrB = "-."; + arrE = ".->"; + optional = false; // Afterwards they should be solid again + } + return indent() + subj + arrB + pred + arrE + " " + obj; + } + + @Override + public void meet(Filter f) throws RuntimeException { + String filterId = "f"+filterCount++; + + FindNodesUsedInFilter visitor = new FindNodesUsedInFilter(filterId); + f.getCondition().visit(visitor); + super.meet(f); + } + + @Override + public void meet(Union u) throws RuntimeException { + rq.add(indent() + "%%or"); + rq.add(indent() + "subgraph union" + unionCount+"l[\" \"]"); + indent += 2; + rq.add(indent() + "style union" + unionCount + "l fill:#abf,stroke-dasharray: 3 3;"); + u.getRightArg().visit(this); + indent -= 2; + rq.add(indent() + "end"); + + rq.add(indent() + "subgraph union" + unionCount+"r[\" \"]"); + indent += 2; + rq.add(indent() + "style union" + unionCount + "r fill:#abf,stroke-dasharray: 3 3;"); + u.getLeftArg().visit(this); + indent -= 2; + rq.add(indent() + "end"); + rq.add(indent() + "union"+unionCount+"r <== or ==> union" + unionCount+"l"); + unionCount++; + } + + @Override + public void meet(LeftJoin s) throws RuntimeException { + s.getLeftArg().visit(this); + optional = true; + rq.add(indent() + "%%optional"); + rq.add(indent() + "subgraph optional" + optionalCount+"[\"(optional)\"]"); + rq.add(indent() + "style optional" + optionalCount++ + " fill:#bbf,stroke-dasharray: 5 5;"); + indent += 2; + s.getRightArg().visit(this); + indent -= 2; + rq.add(indent() + "end"); + } + + @Override + public void meet(Service s) throws RuntimeException { + Value sv = s.getServiceRef().getValue(); + String serviceIri = sv.stringValue(); + String key = serviceKeys.computeIfAbsent(sv, (t) -> "s" + (serviceKeys.size() + 1)); + rq.add(indent() + "subgraph " + key + "[\"" + serviceIri + "\"]"); + indent += 2; + rq.add(indent() + "style " + key + " stroke-width:4px;"); + super.meet(s); + indent -= 2; + rq.add(indent() + "end"); + } + + private String indent() { + return IntStream.range(0, indent).mapToObj(i -> " ").collect(Collectors.joining()); + } + + private String asString(Var v) { + if (v.isAnonymous() && !v.isConstant()) { + return anonymousKeys.get(v.getName()); + } else if (v.isConstant()) { + return constantKeys.get(v.getValue()); + } else { + return variableKeys.get(v.getName()); + } + } + + private static String prefix(Value v, Map iriPrefixes) { + if (v instanceof IRI i) + return prefix(i, iriPrefixes); + else if (v instanceof Literal l) + return prefix(l, iriPrefixes); + else + return v.stringValue(); + } + + private static String prefix(String stringValue, Map iriPrefixes) { + for (Map.Entry en : iriPrefixes.entrySet()) { + if (stringValue.startsWith(en.getKey())) { + return '"' + en.getValue() + stringValue.substring(en.getKey().length()) + '"'; + } + } + return stringValue; + } + + private static String prefix(IRI stringValue, Map iriPrefixes) { + if (RDF.TYPE.equals(stringValue)) { + return "\"a\""; + } else { + return prefix(stringValue.stringValue(), iriPrefixes); + } + } + + private static String prefix(Literal l, Map iriPrefixes) { + CoreDatatype cd = l.getCoreDatatype(); + if (cd.isXSDDatatype()) { + if (CoreDatatype.XSD.STRING.equals(cd)) + return '"' + l.stringValue() + '"'; + else if (cd.isXSDDatatype()) + return '"' + l.stringValue() + "^^xsd:" + cd.getIri().getLocalName() + '"'; + else if (cd.isRDFDatatype()) + return '"' + l.stringValue() + "^^rdf:" + cd.getIri().getLocalName() + '"'; + else if (cd.isGEODatatype()) + return '"' + l.stringValue() + "^^geo:" + cd.getIri().getLocalName() + '"'; + } + return '"' + l.stringValue() + "^^<" + l.getDatatype() + ">\""; + } +} \ No newline at end of file diff --git a/src/test/java/swiss/sib/rdf/sparql/examples/SparqlInRdfToMermaidTest.java b/src/test/java/swiss/sib/rdf/sparql/examples/SparqlInRdfToMermaidTest.java index feee8e44d..9ed80dcdd 100644 --- a/src/test/java/swiss/sib/rdf/sparql/examples/SparqlInRdfToMermaidTest.java +++ b/src/test/java/swiss/sib/rdf/sparql/examples/SparqlInRdfToMermaidTest.java @@ -95,6 +95,7 @@ public class SparqlInRdfToMermaidTest { BIND(strafter(str(?ec),str(ec:)) as ?ecNumber) ?rhea rh:isTransport ?isTransport . ?rhea rh:equation [] . + FILTER(?isTransport = true) }''' . """; String simple = """