diff --git a/CHANGELOG.md b/CHANGELOG.md index 534e4164d5..913d803290 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed * We overhauled the handling of input and output streams for all (de-)serializers. Input and output streams are no longer closed automatically. This is to prevent asymmetric code where we would close a stream that we haven't opened. This is problematic in cases where e.g. `System.out` is passed as an output stream to simply print a serialized automaton and the `System.out` stream would be closed afterwards. Since input and output streams are usually opened in client-code, they should be closed in client-code as well. We suggest to simply wrap calls to the serializers in a try-with-resource block. +* Due to the DOT parsers rewrite (see **Fixed**), the attribute parser now receive a `Map` instead of a `Map`. ### Removed @@ -25,6 +26,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * Correctly enquote outputs containing whitespaces in `TAFWriter` ([#37](https://github.com/LearnLib/automatalib/issues/37), thanks to [Alexander Schieweck](https://github.com/aschieweck)). * Fixed a bug in the `Graph` representation of `AbstractOneSEVPA`s ([#39](https://github.com/LearnLib/automatalib/pull/39), thanks to [DonatoClun](https://github.com/DonatoClun)). +* Replaced the 3rd-party DOT parser with our own implementation to fix the issue that multi-edges between nodes were not properly handled. ## [0.9.0](https://github.com/LearnLib/automatalib/releases/tag/automatalib-0.9.0) - 2020-02-05 diff --git a/build-parent/pom.xml b/build-parent/pom.xml index 44d3d4cc94..e6176f0d76 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -55,6 +55,13 @@ limitations under the License. net/automatalib/util/automata/builders/MooreBuilder.class net/automatalib/util/automata/builders/MooreBuilder$*.class + + net/automatalib/serialization/dot/InternalDOTParser*.class + net/automatalib/serialization/dot/ParseException.class + net/automatalib/serialization/dot/SimpleCharStream.class + net/automatalib/serialization/dot/Token.class + net/automatalib/serialization/dot/TokenMgrError.class + net/automatalib/serialization/taf/parser/InternalTAFParser*.class net/automatalib/serialization/taf/parser/ParseException.class diff --git a/build-tools/src/main/resources/automatalib-spotbugs-exclusions.xml b/build-tools/src/main/resources/automatalib-spotbugs-exclusions.xml index 26c152beff..fe3099c90f 100644 --- a/build-tools/src/main/resources/automatalib-spotbugs-exclusions.xml +++ b/build-tools/src/main/resources/automatalib-spotbugs-exclusions.xml @@ -52,6 +52,14 @@ limitations under the License. + + + + + + + + diff --git a/pom.xml b/pom.xml index 182cbbc1ad..9b06e850b5 100644 --- a/pom.xml +++ b/pom.xml @@ -199,7 +199,6 @@ limitations under the License. 1.9 3.0.0 8.29 - 1.0 0.1.0 9+181-r4173-1 28.2-jre @@ -437,11 +436,6 @@ limitations under the License. checker-qual ${checkerframework.version} - - com.paypal.digraph - digraph-parser - ${digraph-parser.version} - diff --git a/serialization/dot/pom.xml b/serialization/dot/pom.xml index 666f03b998..613ccec9cb 100644 --- a/serialization/dot/pom.xml +++ b/serialization/dot/pom.xml @@ -29,7 +29,7 @@ limitations under the License. jar AutomataLib :: Serialization :: DOT - Serializers for the DOT Format + (De-)Serializers for the DOT Format @@ -55,10 +55,6 @@ limitations under the License. com.google.guava guava - - com.paypal.digraph - digraph-parser - @@ -77,4 +73,13 @@ limitations under the License. testng + + + + + org.codehaus.mojo + javacc-maven-plugin + + + diff --git a/serialization/dot/src/main/java/net/automatalib/serialization/dot/DOTGraphParser.java b/serialization/dot/src/main/java/net/automatalib/serialization/dot/DOTGraphParser.java index a31dd44413..4c64a99b0f 100644 --- a/serialization/dot/src/main/java/net/automatalib/serialization/dot/DOTGraphParser.java +++ b/serialization/dot/src/main/java/net/automatalib/serialization/dot/DOTGraphParser.java @@ -17,19 +17,16 @@ import java.io.IOException; import java.io.InputStream; +import java.io.Reader; +import java.util.Collection; import java.util.Map; import java.util.function.Function; import java.util.function.Supplier; import com.google.common.collect.Maps; -import com.paypal.digraph.parser.GraphEdge; -import com.paypal.digraph.parser.GraphNode; -import com.paypal.digraph.parser.GraphParser; -import com.paypal.digraph.parser.GraphParserException; import net.automatalib.commons.util.IOUtil; import net.automatalib.graphs.Graph; import net.automatalib.graphs.MutableGraph; -import net.automatalib.serialization.FormatException; import net.automatalib.serialization.ModelDeserializer; /** @@ -47,8 +44,8 @@ public class DOTGraphParser> implements ModelDeserializer { private final Supplier creator; - private final Function, NP> nodeParser; - private final Function, EP> edgeParser; + private final Function, NP> nodeParser; + private final Function, EP> edgeParser; /** * Parser for (directed) {@link Graph}s with a custom graph instance and custom node and edge attributes. @@ -61,8 +58,8 @@ public class DOTGraphParser> implem * an edge parser that extracts from a property map of an edge the edge property */ public DOTGraphParser(Supplier creator, - Function, NP> nodeParser, - Function, EP> edgeParser) { + Function, NP> nodeParser, + Function, EP> edgeParser) { this.creator = creator; this.nodeParser = nodeParser; this.edgeParser = edgeParser; @@ -71,33 +68,31 @@ public DOTGraphParser(Supplier creator, @Override public G readModel(InputStream is) throws IOException { - final GraphParser gp; + try (Reader r = IOUtil.asUncompressedBufferedNonClosingUTF8Reader(is)) { + InternalDOTParser parser = new InternalDOTParser(r); + parser.parse(); - try { - gp = new GraphParser(IOUtil.asUncompressedBufferedNonClosingInputStream(is)); - } catch (GraphParserException gpe) { - throw new FormatException(gpe); - } - - final G graph = creator.get(); + final G graph = creator.get(); - parseNodesAndEdges(gp, (MutableGraph) graph); + parseNodesAndEdges(parser, (MutableGraph) graph); - return graph; + return graph; + } } - private void parseNodesAndEdges(GraphParser gp, MutableGraph graph) { - final Map stateMap = Maps.newHashMapWithExpectedSize(gp.getNodes().size()); + private void parseNodesAndEdges(InternalDOTParser parser, MutableGraph graph) { + final Collection nodes = parser.getNodes(); + final Collection edges = parser.getEdges(); + + final Map stateMap = Maps.newHashMapWithExpectedSize(nodes.size()); - for (GraphNode node : gp.getNodes().values()) { - final N n = graph.addNode(nodeParser.apply(node.getAttributes())); - stateMap.put(node.getId(), n); + for (Node node : nodes) { + final N n = graph.addNode(nodeParser.apply(node.attributes)); + stateMap.put(node.id, n); } - for (GraphEdge edge : gp.getEdges().values()) { - graph.connect(stateMap.get(edge.getNode1().getId()), - stateMap.get(edge.getNode2().getId()), - edgeParser.apply(edge.getAttributes())); + for (Edge edge : edges) { + graph.connect(stateMap.get(edge.src), stateMap.get(edge.tgt), edgeParser.apply(edge.attributes)); } } } diff --git a/serialization/dot/src/main/java/net/automatalib/serialization/dot/DOTMutableAutomatonParser.java b/serialization/dot/src/main/java/net/automatalib/serialization/dot/DOTMutableAutomatonParser.java index 94073dd3af..cd34dae3ae 100644 --- a/serialization/dot/src/main/java/net/automatalib/serialization/dot/DOTMutableAutomatonParser.java +++ b/serialization/dot/src/main/java/net/automatalib/serialization/dot/DOTMutableAutomatonParser.java @@ -17,6 +17,7 @@ import java.io.IOException; import java.io.InputStream; +import java.io.Reader; import java.util.Collection; import java.util.HashSet; import java.util.Map; @@ -24,15 +25,10 @@ import java.util.function.Function; import com.google.common.collect.Maps; -import com.paypal.digraph.parser.GraphEdge; -import com.paypal.digraph.parser.GraphNode; -import com.paypal.digraph.parser.GraphParser; -import com.paypal.digraph.parser.GraphParserException; import net.automatalib.automata.AutomatonCreator; import net.automatalib.automata.MutableAutomaton; import net.automatalib.commons.util.IOUtil; import net.automatalib.commons.util.Pair; -import net.automatalib.serialization.FormatException; import net.automatalib.serialization.InputModelData; import net.automatalib.serialization.InputModelDeserializer; import net.automatalib.words.Alphabet; @@ -56,8 +52,8 @@ public class DOTMutableAutomatonParser { private final AutomatonCreator creator; - private final Function, SP> nodeParser; - private final Function, Pair> edgeParser; + private final Function, SP> nodeParser; + private final Function, Pair> edgeParser; private final Collection initialNodeIds; private final boolean fakeInitialNodeIds; @@ -81,8 +77,8 @@ public class DOTMutableAutomatonParser creator, - Function, SP> nodeParser, - Function, Pair> edgeParser, + Function, SP> nodeParser, + Function, Pair> edgeParser, Collection initialNodeIds, boolean fakeInitialNodeIds) { this.creator = creator; @@ -95,55 +91,54 @@ public DOTMutableAutomatonParser(AutomatonCreator creator, @Override public InputModelData readModel(InputStream is) throws IOException { - final GraphParser gp; + try (Reader r = IOUtil.asUncompressedBufferedNonClosingUTF8Reader(is)) { + InternalDOTParser parser = new InternalDOTParser(r); + parser.parse(); - try { - gp = new GraphParser(IOUtil.asUncompressedBufferedNonClosingInputStream(is)); - } catch (GraphParserException gpe) { - throw new FormatException(gpe); - } + assert parser.isDirected(); - final Set inputs = new HashSet<>(); + final Set inputs = new HashSet<>(); - for (GraphEdge edge : gp.getEdges().values()) { - if (!fakeInitialNodeIds || !initialNodeIds.contains(edge.getNode1().getId())) { - inputs.add(edgeParser.apply(edge.getAttributes()).getFirst()); + for (Edge edge : parser.getEdges()) { + if (!fakeInitialNodeIds || !initialNodeIds.contains(edge.src)) { + inputs.add(edgeParser.apply(edge.attributes).getFirst()); + } } - } - final Alphabet alphabet = Alphabets.fromCollection(inputs); - final A automaton = creator.createAutomaton(alphabet, gp.getNodes().size()); + final Alphabet alphabet = Alphabets.fromCollection(inputs); + final A automaton = creator.createAutomaton(alphabet, parser.getNodes().size()); - parseNodesAndEdges(gp, (MutableAutomaton) automaton); + parseNodesAndEdges(parser, (MutableAutomaton) automaton); - return new InputModelData<>(automaton, alphabet); + return new InputModelData<>(automaton, alphabet); + } } - private void parseNodesAndEdges(GraphParser gp, MutableAutomaton automaton) { - final Map stateMap = Maps.newHashMapWithExpectedSize(gp.getNodes().size()); + private void parseNodesAndEdges(InternalDOTParser parser, MutableAutomaton automaton) { + final Map stateMap = Maps.newHashMapWithExpectedSize(parser.getNodes().size()); - for (GraphNode node : gp.getNodes().values()) { + for (Node node : parser.getNodes()) { final S state; - if (fakeInitialNodeIds && initialNodeIds.contains(node.getId())) { + if (fakeInitialNodeIds && initialNodeIds.contains(node.id)) { continue; - } else if (!fakeInitialNodeIds && initialNodeIds.contains(node.getId())) { - state = automaton.addInitialState(nodeParser.apply(node.getAttributes())); + } else if (!fakeInitialNodeIds && initialNodeIds.contains(node.id)) { + state = automaton.addInitialState(nodeParser.apply(node.attributes)); } else { - state = automaton.addState(nodeParser.apply(node.getAttributes())); + state = automaton.addState(nodeParser.apply(node.attributes)); } - stateMap.put(node.getId(), state); + stateMap.put(node.id, state); } - for (GraphEdge edge : gp.getEdges().values()) { - if (fakeInitialNodeIds && initialNodeIds.contains(edge.getNode1().getId())) { - automaton.setInitial(stateMap.get(edge.getNode2().getId()), true); + for (Edge edge : parser.getEdges()) { + if (fakeInitialNodeIds && initialNodeIds.contains(edge.src)) { + automaton.setInitial(stateMap.get(edge.tgt), true); } else { - final Pair property = edgeParser.apply(edge.getAttributes()); - automaton.addTransition(stateMap.get(edge.getNode1().getId()), + final Pair property = edgeParser.apply(edge.attributes); + automaton.addTransition(stateMap.get(edge.src), property.getFirst(), - stateMap.get(edge.getNode2().getId()), + stateMap.get(edge.tgt), property.getSecond()); } } diff --git a/serialization/dot/src/main/java/net/automatalib/serialization/dot/DOTParsers.java b/serialization/dot/src/main/java/net/automatalib/serialization/dot/DOTParsers.java index e05c5b5206..8d6907c5cf 100644 --- a/serialization/dot/src/main/java/net/automatalib/serialization/dot/DOTParsers.java +++ b/serialization/dot/src/main/java/net/automatalib/serialization/dot/DOTParsers.java @@ -56,16 +56,14 @@ public final class DOTParsers { * Node property parser that parses a node's "{@link NodeAttrs#LABEL label}" attribute and returns its {@link * Object#toString() string} representation. Returns {@code null} if the attribute is not specified. */ - public static final Function, @Nullable String> DEFAULT_NODE_PARSER = attr -> { - final Object label = attr.get(NodeAttrs.LABEL); - return label == null ? null : label.toString(); - }; + public static final Function, @Nullable String> DEFAULT_NODE_PARSER = + attr -> attr.get(NodeAttrs.LABEL); /** * Node property parser that returns {@code true} if a node's "{@link NodeAttrs#SHAPE shape}" attribute is specified * and equals "{@link NodeShapes#DOUBLECIRCLE doublecircle}". Returns {@code false} otherwise. */ - public static final Function, Boolean> DEFAULT_FSA_NODE_PARSER = + public static final Function, Boolean> DEFAULT_FSA_NODE_PARSER = attr -> NodeShapes.DOUBLECIRCLE.equals(attr.get(NodeAttrs.SHAPE)); /** @@ -73,13 +71,13 @@ public final class DOTParsers { * /}. Returns the string representation of {@code } as-is. Returns {@code null} if the * attribute does not exist or does not match the expected format. */ - public static final Function, @Nullable String> DEFAULT_MOORE_NODE_PARSER = attr -> { - final Object label = attr.get(NodeAttrs.LABEL); + public static final Function, @Nullable String> DEFAULT_MOORE_NODE_PARSER = attr -> { + final String label = attr.get(NodeAttrs.LABEL); if (label == null) { return null; } - final String[] tokens = label.toString().split("/"); + final String[] tokens = label.split("/"); if (tokens.length != 2) { return null; @@ -92,24 +90,22 @@ public final class DOTParsers { * Edge input parser that parses an edges's "{@link EdgeAttrs#LABEL label}" attribute and returns its {@link * Object#toString() string} representation. Returns {@code null} if the attribute is not specified. */ - public static final Function, @Nullable String> DEFAULT_EDGE_PARSER = attr -> { - final Object label = attr.get(EdgeAttrs.LABEL); - return label == null ? null : label.toString(); - }; + public static final Function, @Nullable String> DEFAULT_EDGE_PARSER = + attr -> attr.get(EdgeAttrs.LABEL); /** * Edge input parser that expects an edge's "{@link EdgeAttrs#LABEL label}" attribute to be of the form {@code * /}. Returns a {@link Pair} object containing the string representation of both components * as-is. Returns {@code null} if the attribute does not exist or does not match the expected format. */ - public static final Function, Pair<@Nullable String, @Nullable String>> + public static final Function, Pair<@Nullable String, @Nullable String>> DEFAULT_MEALY_EDGE_PARSER = attr -> { - final Object label = attr.get(EdgeAttrs.LABEL); + final String label = attr.get(EdgeAttrs.LABEL); if (label == null) { return Pair.of(null, null); } - final String[] tokens = label.toString().split("/"); + final String[] tokens = label.split("/"); if (tokens.length != 2) { return Pair.of(null, null); @@ -146,8 +142,8 @@ private DOTParsers() {} * * @return a DOT {@link InputModelDeserializer} for {@link CompactDFA}s. */ - public static InputModelDeserializer> dfa(Function, Boolean> nodeParser, - Function, I> edgeParser) { + public static InputModelDeserializer> dfa(Function, Boolean> nodeParser, + Function, I> edgeParser) { return fsa(new CompactDFA.Creator<>(), nodeParser, edgeParser); } @@ -177,8 +173,8 @@ public static InputModelDeserializer> dfa(Function InputModelDeserializer> nfa(Function, Boolean> nodeParser, - Function, I> edgeParser) { + public static InputModelDeserializer> nfa(Function, Boolean> nodeParser, + Function, I> edgeParser) { return fsa(new CompactNFA.Creator<>(), nodeParser, edgeParser); } @@ -202,8 +198,8 @@ public static InputModelDeserializer> nfa(Function> InputModelDeserializer fsa(AutomatonCreator creator, - Function, Boolean> nodeParser, - Function, I> edgeParser) { + Function, Boolean> nodeParser, + Function, I> edgeParser) { return fsa(creator, nodeParser, edgeParser, Collections.singleton(GraphDOT.initialLabel(0))); } @@ -230,8 +226,8 @@ public static > InputModelDeserializer fsa(A * @return a DOT {@link InputModelDeserializer} for {@code A}s. */ public static > InputModelDeserializer fsa(AutomatonCreator creator, - Function, Boolean> nodeParser, - Function, I> edgeParser, + Function, Boolean> nodeParser, + Function, I> edgeParser, Collection initialNodeIds) { return fsa(creator, nodeParser, edgeParser, initialNodeIds, true); } @@ -262,8 +258,8 @@ public static > InputModelDeserializer fsa(A * @return a DOT {@link InputModelDeserializer} for {@code A}s. */ public static > InputModelDeserializer fsa(AutomatonCreator creator, - Function, Boolean> nodeParser, - Function, I> edgeParser, + Function, Boolean> nodeParser, + Function, I> edgeParser, Collection initialNodeIds, boolean fakeInitialNodeIds) { return new DOTMutableAutomatonParser<>(creator, @@ -298,7 +294,7 @@ public static > InputModelDeserializer fsa(A * * @return a DOT {@link InputModelDeserializer} for {@link CompactMealy}s. */ - public static InputModelDeserializer> mealy(Function, Pair> edgeParser) { + public static InputModelDeserializer> mealy(Function, Pair> edgeParser) { return mealy(new CompactMealy.Creator<>(), edgeParser); } @@ -322,7 +318,7 @@ public static InputModelDeserializer> mealy(Functio * @return a DOT {@link InputModelDeserializer} for {@code A}s. */ public static > InputModelDeserializer mealy(AutomatonCreator creator, - Function, Pair> edgeParser) { + Function, Pair> edgeParser) { return mealy(creator, edgeParser, GraphDOT.initialLabel(0)); } @@ -349,7 +345,7 @@ public static InputModelDeserializer> mealy(Functio * @return a DOT {@link InputModelDeserializer} for {@code A}s. */ public static > InputModelDeserializer mealy(AutomatonCreator creator, - Function, Pair> edgeParser, + Function, Pair> edgeParser, String initialNodeId) { return mealy(creator, edgeParser, initialNodeId, true); } @@ -380,7 +376,7 @@ public static InputModelDeserializer> mealy(Functio * @return a DOT {@link InputModelDeserializer} for {@code A}s. */ public static > InputModelDeserializer mealy(AutomatonCreator creator, - Function, Pair> edgeParser, + Function, Pair> edgeParser, String initialNodeId, boolean fakeInitialNodeId) { return new DOTMutableAutomatonParser<>(creator, @@ -419,8 +415,8 @@ public static InputModelDeserializer> mealy(Functio * * @return a DOT {@link InputModelDeserializer} for {@link CompactMoore}s. */ - public static InputModelDeserializer> moore(Function, O> nodeParser, - Function, I> edgeParser) { + public static InputModelDeserializer> moore(Function, O> nodeParser, + Function, I> edgeParser) { return moore(new CompactMoore.Creator<>(), nodeParser, edgeParser); } @@ -447,8 +443,8 @@ public static InputModelDeserializer> moore(Functio * @return a DOT {@link InputModelDeserializer} for {@code A}s. */ public static > InputModelDeserializer moore(AutomatonCreator creator, - Function, O> nodeParser, - Function, I> edgeParser) { + Function, O> nodeParser, + Function, I> edgeParser) { return moore(creator, nodeParser, edgeParser, GraphDOT.initialLabel(0)); } @@ -477,8 +473,8 @@ public static InputModelDeserializer> moore(Functio * @return a DOT {@link InputModelDeserializer} for {@code A}s. */ public static > InputModelDeserializer moore(AutomatonCreator creator, - Function, O> nodeParser, - Function, I> edgeParser, + Function, O> nodeParser, + Function, I> edgeParser, String initialNodeId) { return moore(creator, nodeParser, edgeParser, initialNodeId, true); } @@ -511,8 +507,8 @@ public static InputModelDeserializer> moore(Functio * @return a DOT {@link InputModelDeserializer} for {@code A}s. */ public static > InputModelDeserializer moore(AutomatonCreator creator, - Function, O> nodeParser, - Function, I> edgeParser, + Function, O> nodeParser, + Function, I> edgeParser, String initialNodeId, boolean fakeInitialNodeId) { return new DOTMutableAutomatonParser<>(creator, @@ -551,8 +547,8 @@ public static InputModelDeserializer> moore(Functio * * @return a DOT {@link ModelDeserializer} for {@link CompactGraph}s. */ - public static ModelDeserializer> graph(Function, NP> nodeParser, - Function, EP> edgeParser) { + public static ModelDeserializer> graph(Function, NP> nodeParser, + Function, EP> edgeParser) { return graph(CompactGraph::new, nodeParser, edgeParser); } @@ -575,8 +571,8 @@ public static ModelDeserializer> graph(Function> ModelDeserializer graph(Supplier creator, - Function, NP> nodeParser, - Function, EP> edgeParser) { + Function, NP> nodeParser, + Function, EP> edgeParser) { return new DOTGraphParser<>(creator, nodeParser, edgeParser); } diff --git a/serialization/dot/src/main/java/net/automatalib/serialization/dot/Edge.java b/serialization/dot/src/main/java/net/automatalib/serialization/dot/Edge.java new file mode 100644 index 0000000000..da35169658 --- /dev/null +++ b/serialization/dot/src/main/java/net/automatalib/serialization/dot/Edge.java @@ -0,0 +1,36 @@ +/* Copyright (C) 2013-2020 TU Dortmund + * This file is part of AutomataLib, http://www.automatalib.net/. + * + * 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. + */ +package net.automatalib.serialization.dot; + +import java.util.Map; + +/** + * A utility class to aggregate information of an edge of a DOT graph. + * + * @author frohme + */ +public class Edge { + + public final String src; + public final String tgt; + public final Map attributes; + + public Edge(String src, String tgt, Map attributes) { + this.src = src; + this.tgt = tgt; + this.attributes = attributes; + } +} diff --git a/serialization/dot/src/main/java/net/automatalib/serialization/dot/Node.java b/serialization/dot/src/main/java/net/automatalib/serialization/dot/Node.java new file mode 100644 index 0000000000..b9a45b54eb --- /dev/null +++ b/serialization/dot/src/main/java/net/automatalib/serialization/dot/Node.java @@ -0,0 +1,34 @@ +/* Copyright (C) 2013-2020 TU Dortmund + * This file is part of AutomataLib, http://www.automatalib.net/. + * + * 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. + */ +package net.automatalib.serialization.dot; + +import java.util.Map; + +/** + * A utility class to aggregate information of a node of a DOT graph. + * + * @author frohme + */ +public class Node { + + public final String id; + public final Map attributes; + + public Node(String id, Map attributes) { + this.id = id; + this.attributes = attributes; + } +} diff --git a/serialization/dot/src/main/javacc/DOT.jj b/serialization/dot/src/main/javacc/DOT.jj new file mode 100644 index 0000000000..27538d697b --- /dev/null +++ b/serialization/dot/src/main/javacc/DOT.jj @@ -0,0 +1,402 @@ +/* Copyright (C) 2013-2020 TU Dortmund + * This file is part of AutomataLib, http://www.automatalib.net/. + * + * 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. + */ + +options { + LOOKAHEAD = 1; + STATIC = false; + SUPPORT_CLASS_VISIBILITY_PUBLIC = false; +} + +PARSER_BEGIN(InternalDOTParser) + +package net.automatalib.serialization.dot; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.google.common.base.Preconditions; +import net.automatalib.commons.util.strings.StringUtil; +import net.automatalib.serialization.FormatException; + +/** + * The parser is based on the official language definition. + */ +class InternalDOTParser { + + private Map defaultNodeAttrs = new LinkedHashMap(); + private Map defaultEdgeAttrs = new LinkedHashMap(); + + private Map> nodes = new LinkedHashMap>(); + private Map> edges = new LinkedHashMap>(); + + private boolean parsed; + + private boolean strict; + private boolean directed; + + private List transformedNodes; + private List transformedEdges; + + public List getNodes() { + Preconditions.checkArgument(this.parsed, "parse() needs to be called first"); + return this.transformedNodes; + } + + public List getEdges() { + Preconditions.checkArgument(this.parsed, "parse() needs to be called first"); + return this.transformedEdges; + } + + public boolean isStrict() { + Preconditions.checkArgument(this.parsed, "parse() needs to be called first"); + return this.strict; + } + + public boolean isDirected() { + Preconditions.checkArgument(this.parsed, "parse() needs to be called first"); + return this.directed; + } + + public void parse() throws FormatException { + try { + graph(); + } catch (ParseException e) { + throw new FormatException(e); + } + + this.parsed = true; + + final List transformedNodes = new ArrayList(this.nodes.size()); + final List transformedEdges = new ArrayList(this.edges.size()); + + for (Map.Entry> entry : nodes.entrySet()) { + transformedNodes.add(new Node(entry.getKey(), Collections.unmodifiableMap(entry.getValue()))); + } + for (Map.Entry> entry : edges.entrySet()) { + transformedEdges.add(new Edge(entry.getKey().src, entry.getKey().tgt, Collections.unmodifiableMap(entry.getValue()))); + } + + this.transformedNodes = Collections.unmodifiableList(transformedNodes); + this.transformedEdges = Collections.unmodifiableList(transformedEdges); + + // allow garbage collection + this.nodes = null; + this.edges = null; + } + + private void addEdges(List edges, Map attrs) { + Map localAttrs = new LinkedHashMap(this.defaultEdgeAttrs); + localAttrs.putAll(attrs); + + for (EdgePair ep : edges) { + String src = ep.src; + String tgt = ep.tgt; + + this.edges.put(ep, localAttrs); + + // if edges define new states, add them to the node map + this.putIfAbsent(this.nodes, src, new LinkedHashMap(this.defaultNodeAttrs)); + this.putIfAbsent(this.nodes, tgt, new LinkedHashMap(this.defaultNodeAttrs)); + } + } + + /* + * Utility method, since JavaCC parser does not allow Java 8 lambda statements + */ + private V putIfAbsent(Map map, K key, V value) { + if (!map.containsKey(key)) { + map.put(key, value); + return value; + } else { + return map.get(key); + } + } + + /* + * We require identity semantics + */ + private static class EdgePair { + String src; + String tgt; + + EdgePair(String src, String tgt) { + this.src = src; + this.tgt = tgt; + } + + @Override + public String toString() { + return src + " -> " + tgt; + } + } +} + +PARSER_END(InternalDOTParser) + +SKIP : +{ + " " +| "\r" +| "\t" +| "\n" +} + +TOKEN [IGNORE_CASE]: +{ + < DIGRAPH: "digraph" > +| < EDGE: "edge" > +| < GRAPH: "graph" > +| < NODE: "node" > +| < STRICT: "strict" > +| < SUBGRAPH: "subgraph" > +} + +TOKEN: +{ + < LBRACK: "[" > +| < RBRACK: "]" > +| < LCURLY: "{" > +| < RCURLY: "}" > +| < COLON: ":" > +| < SEMICOLON: ";" > +| < COMMA: "," > +| < EQUALS: "=" > +| < EDGEOP: "--" | "->" > +| < ID: | ( | )*> +| < LETTER: ["a"-"z","A"-"Z","\200"-"\377","_"] > +| < NUMBER: ["0"-"9"] > +| < NUMERAL: ["-"]( ("."()+) | ()+ ("." ()+)?)> +} + +SKIP: +{ + < BEGIN_QID: "\"" > : IN_QID +} + + TOKEN: +{ + < QID: ("\\\""|~["\""])+ > +} + + SKIP: +{ + < END_QID: "\"" > : DEFAULT +} + +SKIP: +{ + < BEGIN_LINE_COMMENT1: "//" > : IN_LINE_COMMENT +| < BEGIN_LINE_COMMENT2: "#" > : IN_LINE_COMMENT +| < BEGIN_BLOCK_COMMENT: "/*" > : IN_BLOCK_COMMENT +} + + SKIP: +{ + < END_LINE_COMMENT: "\n" > : DEFAULT +| < ~[] > +} + + + SKIP: +{ + < END_BLOCK_COMMENT: "*/" > : DEFAULT +| < ~[] > +} + +private void graph(): +{} +{ + [ { this.strict = true; }] ( | { this.directed = true; }) [identifier()] stmt_list() +} + +// return the list of contained nodes +private List stmt_list(): +{ + List nodes; + List allNodes = new ArrayList(); +} +{ + (nodes=stmt() { allNodes.addAll(nodes); } [])* + { + return allNodes; + } +} + +// return the list of contained nodes +private List stmt(): +{ + String id; + List nodes = new ArrayList(); + List graphSrc, graphTgt, edgeTgt; +} +{ + // slightly modified grammar to prevent amiguity + // extracted 'id' and 'subgraph' prefix + ( + (id = identifier() { nodes.add(id); } (edgeTgt = edge_stmt(id) { nodes.addAll(edgeTgt); } | identifier() | node_stmt(id))) + | (graphSrc = subgraph() { nodes.addAll(graphSrc); } (graphTgt = edge_stmt_sgr(graphSrc) { nodes.addAll(graphTgt); })?) + | attr_stmt() + ) + { + return nodes; + } +} + +private void attr_stmt(): +{ + Map defaults = null; +} +{ + ( | { defaults = this.defaultNodeAttrs; } | { defaults = this.defaultEdgeAttrs; }) attr_list(defaults) +} + +private void attr_list(Map attrs): +{} +{ + ( [a_list(attrs)] )+ +} + +private void a_list(Map attrs): +{ + String key; + String value; +} +{ + (key=identifier() value=identifier() { if (attrs != null) attrs.put(key, value); } [ | ])+ +} + +// edge_stmt for node_id +// return the list of contained nodes +private List edge_stmt(String src): +{ + List edges = new ArrayList(); + List tgts; + Map attrs = new LinkedHashMap(); +} +{ + tgts = edgeRHS(Collections.singletonList(src), edges) [attr_list(attrs)] + { + addEdges(edges, attrs); + return tgts; + } +} + +// edge_stmt for subgraphs +// return the list of contained nodes +private List edge_stmt_sgr(List srcs): +{ + List edges = new ArrayList(); + List tgts; + Map attrs = new LinkedHashMap(); +} +{ + tgts = edgeRHS(srcs, edges) [attr_list(attrs)] + { + addEdges(edges, attrs); + return tgts; + } +} + +// return the list of contained nodes +// collect edges in the 'edges' parameter +private List edgeRHS(List srcs, List edges): +{ + String tgt; + List tgts; + List nodes = new ArrayList(); +} +{ + ( + // a single target is a non-aggregating edge + (tgt=node_id() { tgts = Collections.singletonList(tgt); } | tgts = subgraph()) + { + for (String s : srcs) { + for (String t : tgts) { + edges.add(new EdgePair(s, t)); + } + } + nodes.addAll(tgts); + srcs = tgts; + } + )+ + { + return nodes; + } +} + +private void node_stmt(String id): +{ + Map attrs = new LinkedHashMap(); +} +{ + [attr_list(attrs)] + { + // If node was already defined, merge attributes + Map localAttrs = this.putIfAbsent(this.nodes, id, new LinkedHashMap()); + localAttrs.putAll(this.defaultNodeAttrs); + localAttrs.putAll(attrs); + } +} + +private String node_id(): +{ + String id; +} +{ + id=identifier() [port()] + { + return id; + } +} + +private void port(): +{} +{ + ((identifier() [ compass_pt()]) | compass_pt()) +} + +// return the list of referenced nodes +private List subgraph(): +{ + List nodes; +} +{ + [ [identifier()]] nodes=stmt_list() + { + return nodes; + } +} + +private void compass_pt(): +{} +{ + ("n" | "ne" | "e" | "se" | "s" | "sw" | "w" | "nw" | "c" | "_") +} + + +private String identifier(): +{ + Token t; +} +{ + t= { return t.toString(); } +| "\"\"" { return ""; } +| t= { return StringUtil.unescapeQuotes(t.toString()); } +} diff --git a/serialization/dot/src/test/java/net/automatalib/serialization/dot/DOTDeserializationTest.java b/serialization/dot/src/test/java/net/automatalib/serialization/dot/DOTDeserializationTest.java index e688ba40a8..962f019f55 100644 --- a/serialization/dot/src/test/java/net/automatalib/serialization/dot/DOTDeserializationTest.java +++ b/serialization/dot/src/test/java/net/automatalib/serialization/dot/DOTDeserializationTest.java @@ -15,8 +15,10 @@ */ package net.automatalib.serialization.dot; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.StringWriter; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; @@ -35,8 +37,10 @@ import net.automatalib.graphs.UniversalGraph; import net.automatalib.serialization.FormatException; import net.automatalib.util.automata.Automata; +import net.automatalib.util.automata.builders.AutomatonBuilders; import net.automatalib.util.automata.fsa.NFAs; import net.automatalib.words.Word; +import net.automatalib.words.impl.Alphabets; import org.testng.Assert; import org.testng.annotations.Test; @@ -153,6 +157,27 @@ public void doNotCloseInputStreamTest() throws IOException { } } + @Test + public void testDuplicateTransitions() throws IOException { + + // @formatter:off + final CompactDFA dfa = AutomatonBuilders.newDFA(Alphabets.closedCharStringRange('a', 'b')) + .withInitial("s0") + .from("s0").on("a", "b").to("s1") + .from("s1").on("a", "b").to("s0") + .withAccepting("s1") + .create(); + // @formatter:on + + final StringWriter w = new StringWriter(); + GraphDOT.write(dfa, dfa.getInputAlphabet(), w); + + final ByteArrayInputStream bais = new ByteArrayInputStream(w.toString().getBytes()); + final DFA parsed = DOTParsers.dfa().readModel(bais).model; + + Assert.assertTrue(Automata.testEquivalence(dfa, parsed, dfa.getInputAlphabet())); + } + private static , EP extends Comparable, N2, E2> void checkGraphEquivalence( UniversalGraph source, UniversalGraph target) { diff --git a/serialization/dot/src/test/java/net/automatalib/serialization/dot/DOTSerializationUtil.java b/serialization/dot/src/test/java/net/automatalib/serialization/dot/DOTSerializationUtil.java index fe603bbb4c..b92e04081d 100644 --- a/serialization/dot/src/test/java/net/automatalib/serialization/dot/DOTSerializationUtil.java +++ b/serialization/dot/src/test/java/net/automatalib/serialization/dot/DOTSerializationUtil.java @@ -40,6 +40,8 @@ final class DOTSerializationUtil { static final String FAULTY_AUTOMATON_RESOURCE = "/faulty_automaton.dot"; static final String FAULTY_GRAPH_RESOURCE = "/faulty_graph.dot"; + static final String PARSER_RESOURCE = "/parser.dot"; + static final CompactDFA DFA; static final CompactNFA NFA; static final CompactMealy MEALY; diff --git a/serialization/dot/src/test/java/net/automatalib/serialization/dot/InternalDOTParserTest.java b/serialization/dot/src/test/java/net/automatalib/serialization/dot/InternalDOTParserTest.java new file mode 100644 index 0000000000..185c8ef72b --- /dev/null +++ b/serialization/dot/src/test/java/net/automatalib/serialization/dot/InternalDOTParserTest.java @@ -0,0 +1,176 @@ +/* Copyright (C) 2013-2020 TU Dortmund + * This file is part of AutomataLib, http://www.automatalib.net/. + * + * 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. + */ +package net.automatalib.serialization.dot; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import net.automatalib.commons.util.IOUtil; +import org.testng.Assert; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +/** + * @author frohme + */ +public class InternalDOTParserTest { + + private List nodes; + private List edges; + + private int nodesChecked; + + @BeforeMethod + public void setUp() throws IOException { + try (InputStream is = InternalDOTParserTest.class.getResourceAsStream(DOTSerializationUtil.PARSER_RESOURCE); + Reader r = IOUtil.asUncompressedBufferedNonClosingUTF8Reader(is)) { + + final InternalDOTParser parser = new InternalDOTParser(r); + parser.parse(); + + this.nodes = parser.getNodes(); + this.edges = new LinkedList<>(parser.getEdges()); + } + } + + @Test + public void checkNodes() { + checkNodeProperties("n1", "label", "Node 1", "color", "blue"); + checkNodeProperties("n2", "label", "Node 2", "style", "dashed", "color", "red"); + checkNodeProperties("", "label", "empty", "style", "dashed"); + checkNodeProperties("n3", "style", "dashed"); + checkNodeProperties("n4", "label", "Sub 1", "style", "dashed"); + checkNodeProperties("n5", "label", "Sub 2", "style", "dashed"); + checkNodeProperties("n6", "style", "dashed"); + checkNodeProperties("n7", "style", "dashed"); + + checkNodeProperties("n10", "style", "dashed", "shape", "octagon"); + checkNodeProperties("n11", "style", "dashed", "shape", "octagon"); + checkNodeProperties("n12", "style", "dashed", "shape", "octagon"); + checkNodeProperties("n13", "style", "dashed", "shape", "octagon"); + checkNodeProperties("n14", "style", "dashed", "shape", "octagon"); + checkNodeProperties("n15", "style", "dashed", "shape", "octagon"); + + Assert.assertEquals(nodesChecked, 14); + } + + @Test + public void checkEdges() { + Assert.assertEquals(edges.size(), 27); + + checkEdgeProperties("n1", "n2", "label", "Input 1", "color", "green"); + checkEdgeProperties("n1", "n2", "label", "Input 2", "color", "red", "style", "solid"); + checkEdgeProperties("n1", "n3", "label", "arg", "color", "green", "style", "dashed"); + checkEdgeProperties("n3", "n2", "label", "arg", "color", "green", "style", "dashed"); + + // subgraph edges + checkEdgeProperties("n3", "n4", "color", "green", "style", "dashed"); + checkEdgeProperties("n3", "n5", "color", "green", "style", "dashed"); + + checkEdgeProperties("n2", "n5", "color", "green", "style", "dashed"); + + checkEdgeProperties("n6", "n1", "color", "green", "style", "dashed"); + checkEdgeProperties("n7", "n1", "color", "green", "style", "dashed"); + + // first set of nested transitions + checkEdgeProperties("n10", "n12", "color", "red", "style", "dashed"); + checkEdgeProperties("n10", "n13", "color", "red", "style", "dashed"); + checkEdgeProperties("n10", "n14", "color", "red", "style", "dashed"); + checkEdgeProperties("n10", "n15", "color", "red", "style", "dashed"); + checkEdgeProperties("n11", "n12", "color", "red", "style", "dashed"); + checkEdgeProperties("n11", "n13", "color", "red", "style", "dashed"); + checkEdgeProperties("n11", "n14", "color", "red", "style", "dashed"); + checkEdgeProperties("n11", "n15", "color", "red", "style", "dashed"); + checkEdgeProperties("n13", "n14", "color", "blue", "style", "dashed"); + checkEdgeProperties("n13", "n15", "color", "blue", "style", "dashed"); + + // second set of nested transitions + checkEdgeProperties("n10", "n12", "color", "green", "style", "dashed"); + checkEdgeProperties("n10", "n13", "color", "green", "style", "dashed"); + checkEdgeProperties("n11", "n12", "color", "green", "style", "dashed"); + checkEdgeProperties("n11", "n13", "color", "green", "style", "dashed"); + checkEdgeProperties("n12", "n14", "color", "green", "style", "dashed"); + checkEdgeProperties("n12", "n15", "color", "green", "style", "dashed"); + checkEdgeProperties("n13", "n14", "color", "green", "style", "dashed"); + checkEdgeProperties("n13", "n15", "color", "green", "style", "dashed"); + + Assert.assertEquals(edges.size(), 0); + } + + private void checkNodeProperties(String id, String... props) { + + assert props.length % 2 == 0; + + Node node = null; + for (Node n : nodes) { + if (id.equals(n.id)) { + node = n; + break; + } + } + + Assert.assertNotNull(node); + Map attributes = node.attributes; + + // Assure that we check all properties + Assert.assertEquals(attributes.size(), props.length / 2); + + for (int i = 0; i < props.length; i += 2) { + String key = props[i]; + String value = props[i + 1]; + + Assert.assertTrue(attributes.containsKey(key)); + Assert.assertEquals(attributes.get(key), value); + } + + nodesChecked++; + } + + private void checkEdgeProperties(String src, String tgt, String... props) { + + assert props.length % 2 == 0; + + Iterator iter = this.edges.iterator(); + + edge: + while (iter.hasNext()) { + Edge edge = iter.next(); + + // potential match + if (src.equals(edge.src) && tgt.equals(edge.tgt) && edge.attributes.size() == props.length / 2) { + for (int i = 0; i < props.length; i += 2) { + String key = props[i]; + String value = props[i + 1]; + + if (!edge.attributes.containsKey(key) || !value.equals(edge.attributes.get(key))) { + // not a match, continue + continue edge; + } + } + + // mark edge as checked by removing it + iter.remove(); + return; + } + } + Assert.fail("Expected edge not found"); + } +} diff --git a/serialization/dot/src/test/resources/parser.dot b/serialization/dot/src/test/resources/parser.dot new file mode 100644 index 0000000000..8fc5e23794 --- /dev/null +++ b/serialization/dot/src/test/resources/parser.dot @@ -0,0 +1,42 @@ +digraph test { + + graph [color = "blue"] + edge [color = "green"] + + n1 [label = "Node 1" color = "blue"] + n2 [label = "Node 2"] + /* + Some + multi-line + comment + */ + node [style = "dashed"] + + n2 [color = "red"] + + "" [label = empty] + + n1 -> n2 [label = "Input 1"] + n1 -> n2 [label = "Input 2" color = "red" style = "solid"] + + # single line comment + + edge [style = "dashed"] + n1 -> n3 -> n2 [label = "arg"] + + n3 -> subgraph { + n4 [label = "Sub 1"] + n5 [label = "Sub 2"] + } + + n2 -> n5 + + subgraph sg { + n6; n7 + } -> n1 + + node [shape=octagon] + + {n10; n11} -> {n12; n13 -> {n14; n15} [color = blue]} [color = red] + {n10; n11} -> {n12; n13} -> {n14; n15} [color=green] +}