• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

LearnLib / automatalib / 13138848026

04 Feb 2025 02:53PM UTC coverage: 92.108% (+2.2%) from 89.877%
13138848026

push

github

mtf90
[maven-release-plugin] prepare release automatalib-0.12.0

16609 of 18032 relevant lines covered (92.11%)

1.7 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.21
/serialization/fsm/src/main/java/net/automatalib/serialization/fsm/parser/FSM2DFAParser.java
1
/* Copyright (C) 2013-2025 TU Dortmund University
2
 * This file is part of AutomataLib <https://automatalib.net>.
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
package net.automatalib.serialization.fsm.parser;
17

18
import java.io.IOException;
19
import java.io.InputStream;
20
import java.io.Reader;
21
import java.io.StreamTokenizer;
22
import java.util.Collection;
23
import java.util.HashMap;
24
import java.util.Map;
25
import java.util.SortedMap;
26
import java.util.TreeMap;
27
import java.util.function.Function;
28

29
import net.automatalib.alphabet.Alphabet;
30
import net.automatalib.alphabet.impl.Alphabets;
31
import net.automatalib.automaton.AutomatonCreator;
32
import net.automatalib.automaton.fsa.DFA;
33
import net.automatalib.automaton.fsa.MutableDFA;
34
import net.automatalib.automaton.fsa.impl.CompactDFA;
35
import net.automatalib.common.util.IOUtil;
36
import net.automatalib.common.util.Pair;
37
import net.automatalib.exception.FormatException;
38
import net.automatalib.serialization.ModelDeserializer;
39
import org.checkerframework.checker.nullness.qual.Nullable;
40

41
/**
42
 * Parses an FSM to a DFA.
43
 *
44
 * @param <I>
45
 *         the input type
46
 * @param <A>
47
 *         the parsed automaton type
48
 */
49
public final class FSM2DFAParser<I, A extends MutableDFA<Integer, I>> extends AbstractFSMParser<I>
50
        implements ModelDeserializer<A> {
51

52
    // some Exception messages
53
    public static final String ACCEPT_NOT_FOUND = "accepting state label (%s) not found";
54
    public static final String ACCEPT_VALUE_NOT_FOUND = "accepting state label value (%s) not found";
55
    public static final String ACCEPT_INDEX_NOT_FOUND = "index for accepting state label (%d) not found";
56

57
    /**
58
     * The index at which the data definition contains acceptance information.
59
     */
60
    private int acceptIndex = -1;
2✔
61

62
    /**
63
     * The string index indicating whether the state is accepting.
64
     */
65
    private int acceptValue = -1;
2✔
66

67
    /**
68
     * A sorted map of states indices, and whether they are accepting.
69
     */
70
    private final SortedMap<Integer, Boolean> states = new TreeMap<>();
2✔
71

72
    /**
73
     * The map of transitions. Like a {@link DFA}, a transition is a pair of a state
74
     * index, and an input, that maps to a next state index.
75
     */
76
    private final Map<Pair<Integer, I>, Integer> transitions = new HashMap<>();
2✔
77

78
    /**
79
     * The variable name in the data definition that tells whether the state is accepting.
80
     */
81
    private final String acceptingDataVariableName;
82

83
    /**
84
     * The string indicating whether the state is accepting.
85
     */
86
    private final String acceptingDataValue;
87

88
    /**
89
     * The creator for the returned automaton.
90
     */
91
    private final AutomatonCreator<A, I> creator;
92

93
    /**
94
     * Constructs an FSM2DFAParser. To parse a DFA use one of the parse() methods.
95
     *
96
     * @param targetInputs
97
     *         A collection containing the inputs which should constitute the input alphabet of the parsed automaton.
98
     *         If {@code null}, the inputs will be automatically gathered from the read FSM file.
99
     * @param inputParser
100
     *         the input parser.
101
     * @param acceptingDataVariableName
102
     *         the variable name for acceptance (see {@link #acceptingDataVariableName})
103
     * @param acceptingDataValue
104
     *         the string for acceptance (see {@link #acceptingDataValue})
105
     * @param creator
106
     *         the creator to construct the returned automaton instance
107
     */
108
    private FSM2DFAParser(@Nullable Collection<? extends I> targetInputs,
109
                          Function<String, I> inputParser,
110
                          String acceptingDataVariableName,
111
                          String acceptingDataValue,
112
                          AutomatonCreator<A, I> creator) {
113
        super(targetInputs, inputParser);
2✔
114
        this.acceptingDataVariableName = acceptingDataVariableName;
2✔
115
        this.acceptingDataValue = acceptingDataValue;
2✔
116
        this.creator = creator;
2✔
117
    }
2✔
118

119
    /**
120
     * Parse a data definition.
121
     * <p>
122
     * This line is only parsed when we still have to find {@link #acceptIndex}, and {@link #acceptValue}.
123
     *
124
     * @throws FSMFormatException
125
     *         when the data definition is illegal.
126
     * @throws IOException
127
     *         see {@link StreamTokenizer#nextToken()}.
128
     */
129
    @Override
130
    protected void parseDataDefinition(StreamTokenizer streamTokenizer) throws IOException, FormatException {
131
        if (acceptIndex == -1 && acceptValue == -1) {
2✔
132

133
            // check we will read an identifier.
134
            if (streamTokenizer.nextToken() != StreamTokenizer.TT_WORD) {
2✔
135
                throw new FSMFormatException(EXPECT_IDENTIFIER, streamTokenizer);
×
136
            }
137

138
            final String dataVariableName = streamTokenizer.sval;
2✔
139

140
            if (acceptingDataVariableName.equals(dataVariableName)) {
2✔
141
                acceptIndex = getPartLineNumber();
2✔
142

143
                // skip a (
144
                if (streamTokenizer.nextToken() != '(') {
2✔
145
                    throw new FSMFormatException(String.format(EXPECT_CHAR, '('), streamTokenizer);
×
146
                }
147

148
                // skip a number
149
                if (streamTokenizer.nextToken() != StreamTokenizer.TT_WORD) {
2✔
150
                    throw new FSMFormatException(EXPECT_NUMBER, streamTokenizer);
×
151
                }
152

153
                // skip a )
154
                if (streamTokenizer.nextToken() != ')') {
2✔
155
                    throw new FSMFormatException(String.format(EXPECT_CHAR, ')'), streamTokenizer);
×
156
                }
157

158
                // skip an identifier
159
                if (streamTokenizer.nextToken() != StreamTokenizer.TT_WORD) {
2✔
160
                    throw new FSMFormatException(EXPECT_IDENTIFIER, streamTokenizer);
×
161
                }
162

163
                int dataValueIndex = 0;
2✔
164

165
                // find the string containing the acceptance information
166
                while (streamTokenizer.nextToken() == '"' && acceptValue == -1) {
2✔
167
                    final String dataValue = streamTokenizer.sval;
2✔
168
                    if (acceptingDataValue.equals(dataValue)) {
2✔
169
                        acceptValue = dataValueIndex;
2✔
170
                    } else {
171
                        dataValueIndex++;
2✔
172
                    }
173
                }
2✔
174
                // push back the EOL, or EOF we accidentally read
175
                streamTokenizer.pushBack();
2✔
176

177
                // throw an Exception when the string containing acceptance information is not found in the current line
178
                if (acceptValue == -1) {
2✔
179
                    throw new FSMFormatException(String.format(ACCEPT_VALUE_NOT_FOUND, acceptingDataValue),
×
180
                                                 streamTokenizer);
181
                }
182
            }
183
        }
184
    }
2✔
185

186
    /**
187
     * Checks the data definition by ensuring the index in the state vector containing acceptance information is
188
     * defined.
189
     *
190
     * @throws FSMFormatException
191
     *         when the acceptance information could not be found.
192
     */
193
    @Override
194
    protected void checkDataDefinitions(StreamTokenizer streamTokenizer) throws FormatException {
195
        if (acceptIndex == -1) {
2✔
196
            throw new FSMFormatException(String.format(ACCEPT_NOT_FOUND, acceptingDataVariableName),
×
197
                                         streamTokenizer);
198
        }
199
    }
2✔
200

201
    /**
202
     * Parse a state vector.
203
     * <p>
204
     * This method will only search for whether the state is accepting or not. The state index will be equal to the
205
     * current {@link #getPartLineNumber()}.
206
     *
207
     * @throws FSMFormatException
208
     *         when the current line is an illegal state vector.
209
     * @throws IOException
210
     *         see {@link StreamTokenizer#nextToken()}.
211
     */
212
    @Override
213
    protected void parseStateVector(StreamTokenizer streamTokenizer) throws IOException, FormatException {
214
        Boolean accepting = null;
2✔
215
        for (int i = 0;
2✔
216
             i <= acceptIndex && streamTokenizer.nextToken() == StreamTokenizer.TT_WORD && accepting == null;
2✔
217
             i++) {
2✔
218
            if (i == acceptIndex) {
2✔
219
                try {
220
                    accepting = acceptValue == Integer.parseInt(streamTokenizer.sval);
2✔
221
                } catch (NumberFormatException nfe) {
×
222
                    throw new FSMFormatException(nfe, streamTokenizer);
×
223
                }
2✔
224
            }
225
        }
226
        if (accepting == null) {
2✔
227
            throw new FSMFormatException(String.format(ACCEPT_INDEX_NOT_FOUND, acceptIndex), streamTokenizer);
×
228
        } else {
229
            states.put(getPartLineNumber(), accepting);
2✔
230
        }
231
    }
2✔
232

233
    /**
234
     * Does nothing.
235
     */
236
    @Override
237
    protected void checkStateVectors(StreamTokenizer streamTokenizer) {}
2✔
238

239
    /**
240
     * Parse a transition by searching the current line for the source state, target state and the input.
241
     *
242
     * @throws FSMFormatException
243
     *         when the current line is an illegal transition.
244
     * @throws IOException
245
     *         see {@link StreamTokenizer#nextToken()}.
246
     */
247
    @Override
248
    protected void parseTransition(StreamTokenizer streamTokenizer) throws IOException, FormatException {
249
        try {
250
            // check whether we will read a number
251
            if (streamTokenizer.nextToken() != StreamTokenizer.TT_WORD) {
2✔
252
                throw new FSMFormatException(EXPECT_NUMBER, streamTokenizer);
×
253
            }
254

255
            // read the source state index
256
            int from = Integer.parseInt(streamTokenizer.sval);
2✔
257

258
            // check if such a state exists
259
            if (!states.isEmpty() && !states.containsKey(from)) {
2✔
260
                throw new FSMFormatException(String.format(NO_SUCH_STATE, from), streamTokenizer);
×
261
            }
262

263
            // check whether we will read a number
264
            if (streamTokenizer.nextToken() != StreamTokenizer.TT_WORD) {
2✔
265
                throw new FSMFormatException(EXPECT_NUMBER, streamTokenizer);
×
266
            }
267

268
            // read the target state
269
            int to = Integer.parseInt(streamTokenizer.sval);
2✔
270

271
            // check if such a state exists
272
            if (!states.isEmpty() && !states.containsKey(to)) {
2✔
273
                throw new FSMFormatException(String.format(NO_SUCH_STATE, to), streamTokenizer);
×
274
            }
275

276
            // check we will read a string
277
            if (streamTokenizer.nextToken() != '"') {
2✔
278
                throw new FSMFormatException(EXPECT_STRING, streamTokenizer);
×
279
            }
280

281
            // read the input on the transition
282
            final I input = getInputParser().apply(streamTokenizer.sval);
2✔
283

284
            // add it to the set of inputs
285
            getInputs().add(input);
2✔
286

287
            // add the new transition
288
            final Integer prev = transitions.put(Pair.of(from, input), to);
2✔
289
            if (prev != null) {
2✔
290
                throw new FSMFormatException(String.format(NON_DETERMINISM_DETECTED, prev), streamTokenizer);
×
291
            }
292
        } catch (NumberFormatException nfe) {
×
293
            throw new FSMFormatException(nfe, streamTokenizer);
×
294
        }
2✔
295
    }
2✔
296

297
    /**
298
     * Do nothing.
299
     */
300
    @Override
301
    protected void checkTransitions(StreamTokenizer streamTokenizer) {
302
        // Only if no states are defined we add all from the transitions we found.
303
        // This is necessary because states are not necessarily defined in FSMs.
304
        if (states.isEmpty()) {
2✔
305
            transitions.keySet().forEach(e -> states.put(e.getFirst(), true));
×
306
        }
307
    }
2✔
308

309
    /**
310
     * Constructs the actual {@link DFA}.
311
     *
312
     * @return the DFA represented by the FSM file.
313
     *
314
     * @throws FSMFormatException
315
     *         see {@link #parse(Reader)}.
316
     * @throws IOException
317
     *         see {@link #parse(Reader)}.
318
     */
319
    private A parseDFA(Reader reader) throws IOException, FormatException {
320

321
        parse(reader);
2✔
322

323
        final Alphabet<I> alphabet;
324

325
        if (targetInputs != null) {
2✔
326
            alphabet = Alphabets.fromCollection(targetInputs);
2✔
327
        } else {
328
            alphabet = Alphabets.fromCollection(getInputs());
2✔
329
        }
330

331
        final A dfa = creator.createAutomaton(alphabet);
2✔
332

333
        // add all the states
334
        states.forEach((key, value) -> dfa.addState(value));
2✔
335

336
        // set the initial state (substract one because the StreamTokenizer starts counting at 1
337
        dfa.setInitialState(states.firstKey() - 1);
2✔
338

339
        // add all the transitions
340
        transitions.entrySet()
2✔
341
                   .stream()
2✔
342
                   .filter(e -> alphabet.containsSymbol(e.getKey().getSecond()))
2✔
343
                   .forEach(e -> dfa.addTransition(e.getKey().getFirst() - 1,
2✔
344
                                                   e.getKey().getSecond(),
2✔
345
                                                   e.getValue() - 1));
2✔
346

347
        // clear our state for next parse call
348
        states.clear();
2✔
349
        transitions.clear();
2✔
350

351
        return dfa;
2✔
352
    }
353

354
    @Override
355
    public A readModel(InputStream is) throws IOException, FormatException {
356
        try (Reader r = IOUtil.asNonClosingUTF8Reader(is)) {
2✔
357
            return parseDFA(r);
2✔
358
        }
359
    }
360

361
    /**
362
     * Constructs a {@link ModelDeserializer} that reads a {@link DFA} description and writes it into a
363
     * {@link CompactDFA}.
364
     *
365
     * @param inputParser
366
     *         the transformer of String representatives to alphabet symbols
367
     * @param acceptingDataValue
368
     *         the label identifying the accepting variable name
369
     * @param acceptingDataVariableName
370
     *         the label identifying the accepting variable value
371
     * @param <I>
372
     *         input symbol type
373
     *
374
     * @return a {@link ModelDeserializer} that reads a {@link DFA} description
375
     */
376
    public static <I> ModelDeserializer<CompactDFA<I>> getParser(Function<String, I> inputParser,
377
                                                                 String acceptingDataVariableName,
378
                                                                 String acceptingDataValue) {
379
        return getParser(null, inputParser, acceptingDataVariableName, acceptingDataValue);
2✔
380
    }
381

382
    /**
383
     * Constructs a {@link ModelDeserializer} that reads a {@link DFA} description and writes it into a
384
     * {@link CompactDFA}, limited to the specified input symbols.
385
     *
386
     * @param targetInputs
387
     *         the collection of symbols the parsing should be constrained to (may be {@code null} if unconstrained)
388
     * @param inputParser
389
     *         the transformer of String representatives to alphabet symbols
390
     * @param acceptingDataValue
391
     *         the label identifying the accepting variable name
392
     * @param acceptingDataVariableName
393
     *         the label identifying the accepting variable value
394
     * @param <I>
395
     *         input symbol type
396
     *
397
     * @return a {@link ModelDeserializer} that reads a {@link DFA} description
398
     */
399
    public static <I> ModelDeserializer<CompactDFA<I>> getParser(@Nullable Collection<? extends I> targetInputs,
400
                                                                 Function<String, I> inputParser,
401
                                                                 String acceptingDataVariableName,
402
                                                                 String acceptingDataValue) {
403
        return getParser(targetInputs,
2✔
404
                         inputParser,
405
                         acceptingDataVariableName,
406
                         acceptingDataValue,
407
                         new CompactDFA.Creator<>());
408
    }
409

410
    /**
411
     * Constructs a {@link ModelDeserializer} that reads a {@link DFA} description and writes it into a given
412
     * {@link MutableDFA}, limited to the specified input symbols.
413
     *
414
     * @param targetInputs
415
     *         the collection of symbols the parsing should be constrained to (may be {@code null} if unconstrained)
416
     * @param inputParser
417
     *         the transformer of String representatives to alphabet symbols
418
     * @param acceptingDataValue
419
     *         the label identifying the accepting variable name
420
     * @param acceptingDataVariableName
421
     *         the label identifying the accepting variable value
422
     * @param creator
423
     *         the creator to construct the concrete automaton instance
424
     * @param <I>
425
     *         input symbol type
426
     * @param <A>
427
     *         (concrete) automaton type
428
     *
429
     * @return a {@link ModelDeserializer} that reads a {@link DFA} description
430
     */
431
    public static <I, A extends MutableDFA<Integer, I>> ModelDeserializer<A> getParser(@Nullable Collection<? extends I> targetInputs,
432
                                                                                       Function<String, I> inputParser,
433
                                                                                       String acceptingDataVariableName,
434
                                                                                       String acceptingDataValue,
435
                                                                                       AutomatonCreator<A, I> creator) {
436
        return new FSM2DFAParser<>(targetInputs, inputParser, acceptingDataVariableName, acceptingDataValue, creator);
2✔
437
    }
438
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc