1/*
2 * Copyright (C) 2011 The Android Open Source Project
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
17
18package android.filterfw.io;
19
20import java.lang.Float;
21import java.lang.Integer;
22import java.lang.String;
23
24import java.util.ArrayList;
25import java.util.regex.Pattern;
26
27import android.filterfw.core.Filter;
28import android.filterfw.core.FilterFactory;
29import android.filterfw.core.FilterGraph;
30import android.filterfw.core.KeyValueMap;
31import android.filterfw.core.ProtocolException;
32import android.filterfw.io.GraphReader;
33import android.filterfw.io.GraphIOException;
34import android.filterfw.io.PatternScanner;
35
36/**
37 * @hide
38 */
39public class TextGraphReader extends GraphReader {
40
41    private ArrayList<Command> mCommands = new ArrayList<Command>();
42    private Filter mCurrentFilter;
43    private FilterGraph mCurrentGraph;
44    private KeyValueMap mBoundReferences;
45    private KeyValueMap mSettings;
46    private FilterFactory mFactory;
47
48    private interface Command {
49        public void execute(TextGraphReader reader) throws GraphIOException;
50    }
51
52    private class ImportPackageCommand implements Command {
53        private String mPackageName;
54
55        public ImportPackageCommand(String packageName) {
56            mPackageName = packageName;
57        }
58
59        @Override
60        public void execute(TextGraphReader reader) throws GraphIOException {
61            try {
62                reader.mFactory.addPackage(mPackageName);
63            } catch (IllegalArgumentException e) {
64                throw new GraphIOException(e.getMessage());
65            }
66        }
67    }
68
69    private class AddLibraryCommand implements Command {
70        private String mLibraryName;
71
72        public AddLibraryCommand(String libraryName) {
73            mLibraryName = libraryName;
74        }
75
76        @Override
77        public void execute(TextGraphReader reader) {
78            reader.mFactory.addFilterLibrary(mLibraryName);
79        }
80    }
81
82    private class AllocateFilterCommand implements Command {
83        private String mClassName;
84        private String mFilterName;
85
86        public AllocateFilterCommand(String className, String filterName) {
87            mClassName = className;
88            mFilterName = filterName;
89        }
90
91        public void execute(TextGraphReader reader) throws GraphIOException {
92            // Create the filter
93            Filter filter = null;
94            try {
95                filter = reader.mFactory.createFilterByClassName(mClassName, mFilterName);
96            } catch (IllegalArgumentException e) {
97                throw new GraphIOException(e.getMessage());
98            }
99
100            // Set it as the current filter
101            reader.mCurrentFilter = filter;
102        }
103    }
104
105    private class InitFilterCommand implements Command {
106        private KeyValueMap mParams;
107
108        public InitFilterCommand(KeyValueMap params) {
109            mParams = params;
110        }
111
112        @Override
113        public void execute(TextGraphReader reader) throws GraphIOException {
114            Filter filter = reader.mCurrentFilter;
115            try {
116                filter.initWithValueMap(mParams);
117            } catch (ProtocolException e) {
118                throw new GraphIOException(e.getMessage());
119            }
120            reader.mCurrentGraph.addFilter(mCurrentFilter);
121        }
122    }
123
124    private class ConnectCommand implements Command {
125        private String mSourceFilter;
126        private String mSourcePort;
127        private String mTargetFilter;
128        private String mTargetName;
129
130        public ConnectCommand(String sourceFilter,
131                              String sourcePort,
132                              String targetFilter,
133                              String targetName) {
134            mSourceFilter = sourceFilter;
135            mSourcePort = sourcePort;
136            mTargetFilter = targetFilter;
137            mTargetName = targetName;
138        }
139
140        @Override
141        public void execute(TextGraphReader reader) {
142            reader.mCurrentGraph.connect(mSourceFilter, mSourcePort, mTargetFilter, mTargetName);
143        }
144    }
145
146    @Override
147    public FilterGraph readGraphString(String graphString) throws GraphIOException {
148        FilterGraph result = new FilterGraph();
149
150        reset();
151        mCurrentGraph = result;
152        parseString(graphString);
153        applySettings();
154        executeCommands();
155        reset();
156
157        return result;
158    }
159
160    private void reset() {
161        mCurrentGraph = null;
162        mCurrentFilter = null;
163        mCommands.clear();
164        mBoundReferences = new KeyValueMap();
165        mSettings = new KeyValueMap();
166        mFactory = new FilterFactory();
167    }
168
169    private void parseString(String graphString) throws GraphIOException {
170        final Pattern commandPattern = Pattern.compile("@[a-zA-Z]+");
171        final Pattern curlyClosePattern = Pattern.compile("\\}");
172        final Pattern curlyOpenPattern = Pattern.compile("\\{");
173        final Pattern ignorePattern = Pattern.compile("(\\s+|//[^\\n]*\\n)+");
174        final Pattern packageNamePattern = Pattern.compile("[a-zA-Z\\.]+");
175        final Pattern libraryNamePattern = Pattern.compile("[a-zA-Z\\./:]+");
176        final Pattern portPattern = Pattern.compile("\\[[a-zA-Z0-9\\-_]+\\]");
177        final Pattern rightArrowPattern = Pattern.compile("=>");
178        final Pattern semicolonPattern = Pattern.compile(";");
179        final Pattern wordPattern = Pattern.compile("[a-zA-Z0-9\\-_]+");
180
181        final int STATE_COMMAND           = 0;
182        final int STATE_IMPORT_PKG        = 1;
183        final int STATE_ADD_LIBRARY       = 2;
184        final int STATE_FILTER_CLASS      = 3;
185        final int STATE_FILTER_NAME       = 4;
186        final int STATE_CURLY_OPEN        = 5;
187        final int STATE_PARAMETERS        = 6;
188        final int STATE_CURLY_CLOSE       = 7;
189        final int STATE_SOURCE_FILTERNAME = 8;
190        final int STATE_SOURCE_PORT       = 9;
191        final int STATE_RIGHT_ARROW       = 10;
192        final int STATE_TARGET_FILTERNAME = 11;
193        final int STATE_TARGET_PORT       = 12;
194        final int STATE_ASSIGNMENT        = 13;
195        final int STATE_EXTERNAL          = 14;
196        final int STATE_SETTING           = 15;
197        final int STATE_SEMICOLON         = 16;
198
199        int state = STATE_COMMAND;
200        PatternScanner scanner = new PatternScanner(graphString, ignorePattern);
201
202        String curClassName = null;
203        String curSourceFilterName = null;
204        String curSourcePortName = null;
205        String curTargetFilterName = null;
206        String curTargetPortName = null;
207
208        // State machine main loop
209        while (!scanner.atEnd()) {
210            switch (state) {
211                case STATE_COMMAND: {
212                    String curCommand = scanner.eat(commandPattern, "<command>");
213                    if (curCommand.equals("@import")) {
214                        state = STATE_IMPORT_PKG;
215                    } else if (curCommand.equals("@library")) {
216                        state = STATE_ADD_LIBRARY;
217                    } else if (curCommand.equals("@filter")) {
218                        state = STATE_FILTER_CLASS;
219                    } else if (curCommand.equals("@connect")) {
220                        state = STATE_SOURCE_FILTERNAME;
221                    } else if (curCommand.equals("@set")) {
222                        state = STATE_ASSIGNMENT;
223                    } else if (curCommand.equals("@external")) {
224                        state = STATE_EXTERNAL;
225                    } else if (curCommand.equals("@setting")) {
226                        state = STATE_SETTING;
227                    } else {
228                        throw new GraphIOException("Unknown command '" + curCommand + "'!");
229                    }
230                    break;
231                }
232
233                case STATE_IMPORT_PKG: {
234                    String packageName = scanner.eat(packageNamePattern, "<package-name>");
235                    mCommands.add(new ImportPackageCommand(packageName));
236                    state = STATE_SEMICOLON;
237                    break;
238                }
239
240                case STATE_ADD_LIBRARY: {
241                    String libraryName = scanner.eat(libraryNamePattern, "<library-name>");
242                    mCommands.add(new AddLibraryCommand(libraryName));
243                    state = STATE_SEMICOLON;
244                    break;
245                }
246
247                case STATE_FILTER_CLASS:
248                    curClassName = scanner.eat(wordPattern, "<class-name>");
249                    state = STATE_FILTER_NAME;
250                    break;
251
252                case STATE_FILTER_NAME: {
253                    String curFilterName = scanner.eat(wordPattern, "<filter-name>");
254                    mCommands.add(new AllocateFilterCommand(curClassName, curFilterName));
255                    state = STATE_CURLY_OPEN;
256                    break;
257                }
258
259                case STATE_CURLY_OPEN:
260                    scanner.eat(curlyOpenPattern, "{");
261                    state = STATE_PARAMETERS;
262                    break;
263
264                case STATE_PARAMETERS: {
265                    KeyValueMap params = readKeyValueAssignments(scanner, curlyClosePattern);
266                    mCommands.add(new InitFilterCommand(params));
267                    state = STATE_CURLY_CLOSE;
268                    break;
269                }
270
271                case STATE_CURLY_CLOSE:
272                    scanner.eat(curlyClosePattern, "}");
273                    state = STATE_COMMAND;
274                    break;
275
276                case STATE_SOURCE_FILTERNAME:
277                    curSourceFilterName = scanner.eat(wordPattern, "<source-filter-name>");
278                    state = STATE_SOURCE_PORT;
279                    break;
280
281                case STATE_SOURCE_PORT: {
282                    String portString = scanner.eat(portPattern, "[<source-port-name>]");
283                    curSourcePortName = portString.substring(1, portString.length() - 1);
284                    state = STATE_RIGHT_ARROW;
285                    break;
286                }
287
288                case STATE_RIGHT_ARROW:
289                    scanner.eat(rightArrowPattern, "=>");
290                    state = STATE_TARGET_FILTERNAME;
291                    break;
292
293                case STATE_TARGET_FILTERNAME:
294                    curTargetFilterName = scanner.eat(wordPattern, "<target-filter-name>");
295                    state = STATE_TARGET_PORT;
296                    break;
297
298                case STATE_TARGET_PORT: {
299                    String portString = scanner.eat(portPattern, "[<target-port-name>]");
300                    curTargetPortName = portString.substring(1, portString.length() - 1);
301                    mCommands.add(new ConnectCommand(curSourceFilterName,
302                                                     curSourcePortName,
303                                                     curTargetFilterName,
304                                                     curTargetPortName));
305                    state = STATE_SEMICOLON;
306                    break;
307                }
308
309                case STATE_ASSIGNMENT: {
310                    KeyValueMap assignment = readKeyValueAssignments(scanner, semicolonPattern);
311                    mBoundReferences.putAll(assignment);
312                    state = STATE_SEMICOLON;
313                    break;
314                }
315
316                case STATE_EXTERNAL: {
317                    String externalName = scanner.eat(wordPattern, "<external-identifier>");
318                    bindExternal(externalName);
319                    state = STATE_SEMICOLON;
320                    break;
321                }
322
323                case STATE_SETTING: {
324                    KeyValueMap setting = readKeyValueAssignments(scanner, semicolonPattern);
325                    mSettings.putAll(setting);
326                    state = STATE_SEMICOLON;
327                    break;
328                }
329
330                case STATE_SEMICOLON:
331                    scanner.eat(semicolonPattern, ";");
332                    state = STATE_COMMAND;
333                    break;
334            }
335        }
336
337        // Make sure end of input was expected
338        if (state != STATE_SEMICOLON && state != STATE_COMMAND) {
339            throw new GraphIOException("Unexpected end of input!");
340        }
341    }
342
343    @Override
344    public KeyValueMap readKeyValueAssignments(String assignments) throws GraphIOException {
345        final Pattern ignorePattern = Pattern.compile("\\s+");
346        PatternScanner scanner = new PatternScanner(assignments, ignorePattern);
347        return readKeyValueAssignments(scanner, null);
348    }
349
350    private KeyValueMap readKeyValueAssignments(PatternScanner scanner,
351                                                Pattern endPattern) throws GraphIOException {
352        // Our parser is a state-machine, and these are our states
353        final int STATE_IDENTIFIER = 0;
354        final int STATE_EQUALS     = 1;
355        final int STATE_VALUE      = 2;
356        final int STATE_POST_VALUE = 3;
357
358        final Pattern equalsPattern = Pattern.compile("=");
359        final Pattern semicolonPattern = Pattern.compile(";");
360        final Pattern wordPattern = Pattern.compile("[a-zA-Z]+[a-zA-Z0-9]*");
361        final Pattern stringPattern = Pattern.compile("'[^']*'|\\\"[^\\\"]*\\\"");
362        final Pattern intPattern = Pattern.compile("[0-9]+");
363        final Pattern floatPattern = Pattern.compile("[0-9]*\\.[0-9]+f?");
364        final Pattern referencePattern = Pattern.compile("\\$[a-zA-Z]+[a-zA-Z0-9]");
365        final Pattern booleanPattern = Pattern.compile("true|false");
366
367        int state = STATE_IDENTIFIER;
368        KeyValueMap newVals = new KeyValueMap();
369        String curKey = null;
370        String curValue = null;
371
372        while (!scanner.atEnd() && !(endPattern != null && scanner.peek(endPattern))) {
373            switch (state) {
374                case STATE_IDENTIFIER:
375                    curKey = scanner.eat(wordPattern, "<identifier>");
376                    state = STATE_EQUALS;
377                    break;
378
379                case STATE_EQUALS:
380                    scanner.eat(equalsPattern, "=");
381                    state = STATE_VALUE;
382                    break;
383
384                case STATE_VALUE:
385                    if ((curValue = scanner.tryEat(stringPattern)) != null) {
386                        newVals.put(curKey, curValue.substring(1, curValue.length() - 1));
387                    } else if ((curValue = scanner.tryEat(referencePattern)) != null) {
388                        String refName = curValue.substring(1, curValue.length());
389                        Object referencedObject = mBoundReferences != null
390                            ? mBoundReferences.get(refName)
391                            : null;
392                        if (referencedObject == null) {
393                            throw new GraphIOException(
394                                "Unknown object reference to '" + refName + "'!");
395                        }
396                        newVals.put(curKey, referencedObject);
397                    } else if ((curValue = scanner.tryEat(booleanPattern)) != null) {
398                        newVals.put(curKey, Boolean.parseBoolean(curValue));
399                    } else if ((curValue = scanner.tryEat(floatPattern)) != null) {
400                        newVals.put(curKey, Float.parseFloat(curValue));
401                    } else if ((curValue = scanner.tryEat(intPattern)) != null) {
402                        newVals.put(curKey, Integer.parseInt(curValue));
403                    } else {
404                        throw new GraphIOException(scanner.unexpectedTokenMessage("<value>"));
405                    }
406                    state = STATE_POST_VALUE;
407                    break;
408
409                case STATE_POST_VALUE:
410                    scanner.eat(semicolonPattern, ";");
411                    state = STATE_IDENTIFIER;
412                    break;
413            }
414        }
415
416        // Make sure end is expected
417        if (state != STATE_IDENTIFIER && state != STATE_POST_VALUE) {
418            throw new GraphIOException(
419                "Unexpected end of assignments on line " + scanner.lineNo() + "!");
420        }
421
422        return newVals;
423    }
424
425    private void bindExternal(String name) throws GraphIOException {
426        if (mReferences.containsKey(name)) {
427            Object value = mReferences.get(name);
428            mBoundReferences.put(name, value);
429        } else {
430            throw new GraphIOException("Unknown external variable '" + name + "'! "
431                + "You must add a reference to this external in the host program using "
432                + "addReference(...)!");
433        }
434    }
435
436    /**
437     * Unused for now: Often you may want to declare references that are NOT in a certain graph,
438     * e.g. when reading multiple graphs with the same reader. We could print a warning, but even
439     * that may be too much.
440     **/
441    private void checkReferences() throws GraphIOException {
442        for (String reference : mReferences.keySet()) {
443            if (!mBoundReferences.containsKey(reference)) {
444                throw new GraphIOException(
445                    "Host program specifies reference to '" + reference + "', which is not "
446                    + "declared @external in graph file!");
447            }
448        }
449    }
450
451    private void applySettings() throws GraphIOException {
452        for (String setting : mSettings.keySet()) {
453            Object value = mSettings.get(setting);
454            if (setting.equals("autoBranch")) {
455                expectSettingClass(setting, value, String.class);
456                if (value.equals("synced")) {
457                    mCurrentGraph.setAutoBranchMode(FilterGraph.AUTOBRANCH_SYNCED);
458                } else if (value.equals("unsynced")) {
459                    mCurrentGraph.setAutoBranchMode(FilterGraph.AUTOBRANCH_UNSYNCED);
460                } else if (value.equals("off")) {
461                    mCurrentGraph.setAutoBranchMode(FilterGraph.AUTOBRANCH_OFF);
462                } else {
463                    throw new GraphIOException("Unknown autobranch setting: " + value + "!");
464                }
465            } else if (setting.equals("discardUnconnectedOutputs")) {
466                expectSettingClass(setting, value, Boolean.class);
467                mCurrentGraph.setDiscardUnconnectedOutputs((Boolean)value);
468            } else {
469                throw new GraphIOException("Unknown @setting '" + setting + "'!");
470            }
471        }
472    }
473
474    private void expectSettingClass(String setting,
475                                    Object value,
476                                    Class expectedClass) throws GraphIOException {
477        if (value.getClass() != expectedClass) {
478            throw new GraphIOException("Setting '" + setting + "' must have a value of type "
479                + expectedClass.getSimpleName() + ", but found a value of type "
480                + value.getClass().getSimpleName() + "!");
481        }
482    }
483
484    private void executeCommands() throws GraphIOException {
485        for (Command command : mCommands) {
486            command.execute(this);
487        }
488    }
489}
490