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
17package androidx.media.filterfw;
18
19import android.text.TextUtils;
20
21import java.io.InputStream;
22import java.io.IOException;
23import java.io.StringReader;
24import java.util.ArrayList;
25
26import javax.xml.parsers.ParserConfigurationException;
27import javax.xml.parsers.SAXParser;
28import javax.xml.parsers.SAXParserFactory;
29
30import org.xml.sax.Attributes;
31import org.xml.sax.InputSource;
32import org.xml.sax.SAXException;
33import org.xml.sax.XMLReader;
34import org.xml.sax.helpers.DefaultHandler;
35
36/**
37 * A GraphReader allows obtaining filter graphs from XML graph files or strings.
38 */
39public class GraphReader {
40
41    private static interface Command {
42        public void execute(CommandStack stack);
43    }
44
45    private static class CommandStack {
46        private ArrayList<Command> mCommands = new ArrayList<Command>();
47        private FilterGraph.Builder mBuilder;
48        private FilterFactory mFactory;
49        private MffContext mContext;
50
51        public CommandStack(MffContext context) {
52            mContext = context;
53            mBuilder = new FilterGraph.Builder(mContext);
54            mFactory = new FilterFactory();
55        }
56
57        public void execute() {
58            for (Command command : mCommands) {
59                command.execute(this);
60            }
61        }
62
63        public void append(Command command) {
64            mCommands.add(command);
65        }
66
67        public FilterFactory getFactory() {
68            return mFactory;
69        }
70
71        public MffContext getContext() {
72            return mContext;
73        }
74
75        protected FilterGraph.Builder getBuilder() {
76            return mBuilder;
77        }
78    }
79
80    private static class ImportPackageCommand implements Command {
81        private String mPackageName;
82
83        public ImportPackageCommand(String packageName) {
84            mPackageName = packageName;
85        }
86
87        @Override
88        public void execute(CommandStack stack) {
89            try {
90                stack.getFactory().addPackage(mPackageName);
91            } catch (IllegalArgumentException e) {
92                throw new RuntimeException(e.getMessage());
93            }
94        }
95    }
96
97    private static class AddLibraryCommand implements Command {
98        private String mLibraryName;
99
100        public AddLibraryCommand(String libraryName) {
101            mLibraryName = libraryName;
102        }
103
104        @Override
105        public void execute(CommandStack stack) {
106            FilterFactory.addFilterLibrary(mLibraryName);
107        }
108    }
109
110    private static class AllocateFilterCommand implements Command {
111        private String mClassName;
112        private String mFilterName;
113
114        public AllocateFilterCommand(String className, String filterName) {
115            mClassName = className;
116            mFilterName = filterName;
117        }
118
119        @Override
120        public void execute(CommandStack stack) {
121            Filter filter = null;
122            try {
123                filter = stack.getFactory().createFilterByClassName(mClassName,
124                                                                    mFilterName,
125                                                                    stack.getContext());
126            } catch (IllegalArgumentException e) {
127                throw new RuntimeException("Error creating filter " + mFilterName + "!", e);
128            }
129            stack.getBuilder().addFilter(filter);
130        }
131    }
132
133    private static class AddSourceSlotCommand implements Command {
134        private String mName;
135        private String mSlotName;
136
137        public AddSourceSlotCommand(String name, String slotName) {
138            mName = name;
139            mSlotName = slotName;
140        }
141
142        @Override
143        public void execute(CommandStack stack) {
144            stack.getBuilder().addFrameSlotSource(mName, mSlotName);
145        }
146    }
147
148    private static class AddTargetSlotCommand implements Command {
149        private String mName;
150        private String mSlotName;
151
152        public AddTargetSlotCommand(String name, String slotName) {
153            mName = name;
154            mSlotName = slotName;
155        }
156
157        @Override
158        public void execute(CommandStack stack) {
159            stack.getBuilder().addFrameSlotTarget(mName, mSlotName);
160        }
161    }
162
163    private static class AddVariableCommand implements Command {
164        private String mName;
165        private Object mValue;
166
167        public AddVariableCommand(String name, Object value) {
168            mName = name;
169            mValue = value;
170        }
171
172        @Override
173        public void execute(CommandStack stack) {
174            stack.getBuilder().addVariable(mName, mValue);
175        }
176    }
177
178    private static class SetFilterInputCommand implements Command {
179        private String mFilterName;
180        private String mFilterInput;
181        private Object mValue;
182
183        public SetFilterInputCommand(String filterName, String input, Object value) {
184            mFilterName = filterName;
185            mFilterInput = input;
186            mValue = value;
187        }
188
189        @Override
190        public void execute(CommandStack stack) {
191            if (mValue instanceof Variable) {
192                String varName = ((Variable)mValue).name;
193                stack.getBuilder().assignVariableToFilterInput(varName, mFilterName, mFilterInput);
194            } else {
195                stack.getBuilder().assignValueToFilterInput(mValue, mFilterName, mFilterInput);
196            }
197        }
198    }
199
200    private static class ConnectCommand implements Command {
201        private String mSourceFilter;
202        private String mSourcePort;
203        private String mTargetFilter;
204        private String mTargetPort;
205
206        public ConnectCommand(String sourceFilter,
207                              String sourcePort,
208                              String targetFilter,
209                              String targetPort) {
210            mSourceFilter = sourceFilter;
211            mSourcePort = sourcePort;
212            mTargetFilter = targetFilter;
213            mTargetPort = targetPort;
214        }
215
216        @Override
217        public void execute(CommandStack stack) {
218            stack.getBuilder().connect(mSourceFilter, mSourcePort, mTargetFilter, mTargetPort);
219        }
220    }
221
222    private static class Variable {
223        public String name;
224
225        public Variable(String name) {
226            this.name = name;
227        }
228    }
229
230    private static class XmlGraphReader {
231
232        private SAXParserFactory mParserFactory;
233
234        private static class GraphDataHandler extends DefaultHandler {
235
236            private CommandStack mCommandStack;
237            private boolean mInGraph = false;
238            private String mCurFilterName = null;
239
240            public GraphDataHandler(CommandStack commandStack) {
241                mCommandStack = commandStack;
242            }
243
244            @Override
245            public void startElement(String uri, String localName, String qName, Attributes attr)
246                    throws SAXException {
247                if (localName.equals("graph")) {
248                    beginGraph();
249                } else {
250                    assertInGraph(localName);
251                    if (localName.equals("import")) {
252                        addImportCommand(attr);
253                    } else if (localName.equals("library")) {
254                        addLibraryCommand(attr);
255                    } else if (localName.equals("connect")) {
256                        addConnectCommand(attr);
257                    } else if (localName.equals("var")) {
258                        addVarCommand(attr);
259                    } else if (localName.equals("filter")) {
260                        beginFilter(attr);
261                    } else if (localName.equals("input")) {
262                        addFilterInput(attr);
263                    } else {
264                        throw new SAXException("Unknown XML element '" + localName + "'!");
265                    }
266                }
267            }
268
269            @Override
270            public void endElement (String uri, String localName, String qName) {
271                if (localName.equals("graph")) {
272                    endGraph();
273                } else if (localName.equals("filter")) {
274                    endFilter();
275                }
276            }
277
278            private void addImportCommand(Attributes attributes) throws SAXException {
279                String packageName = getRequiredAttribute(attributes, "package");
280                mCommandStack.append(new ImportPackageCommand(packageName));
281            }
282
283            private void addLibraryCommand(Attributes attributes) throws SAXException {
284                String libraryName = getRequiredAttribute(attributes, "name");
285                mCommandStack.append(new AddLibraryCommand(libraryName));
286            }
287
288            private void addConnectCommand(Attributes attributes) {
289                String sourcePortName   = null;
290                String sourceFilterName = null;
291                String targetPortName   = null;
292                String targetFilterName = null;
293
294                // check for shorthand: <connect source="filter:port" target="filter:port"/>
295                String sourceTag = attributes.getValue("source");
296                if (sourceTag != null) {
297                    String[] sourceParts = sourceTag.split(":");
298                    if (sourceParts.length == 2) {
299                        sourceFilterName = sourceParts[0];
300                        sourcePortName   = sourceParts[1];
301                    } else {
302                        throw new RuntimeException(
303                            "'source' tag needs to have format \"filter:port\"! " +
304                            "Alternatively, you may use the form " +
305                            "'sourceFilter=\"filter\" sourcePort=\"port\"'.");
306                    }
307                } else {
308                    sourceFilterName = attributes.getValue("sourceFilter");
309                    sourcePortName   = attributes.getValue("sourcePort");
310                }
311
312                String targetTag = attributes.getValue("target");
313                if (targetTag != null) {
314                    String[] targetParts = targetTag.split(":");
315                    if (targetParts.length == 2) {
316                        targetFilterName = targetParts[0];
317                        targetPortName   = targetParts[1];
318                    } else {
319                        throw new RuntimeException(
320                            "'target' tag needs to have format \"filter:port\"! " +
321                            "Alternatively, you may use the form " +
322                            "'targetFilter=\"filter\" targetPort=\"port\"'.");
323                    }
324                } else {
325                    targetFilterName = attributes.getValue("targetFilter");
326                    targetPortName   = attributes.getValue("targetPort");
327                }
328
329                String sourceSlotName = attributes.getValue("sourceSlot");
330                String targetSlotName = attributes.getValue("targetSlot");
331                if (sourceSlotName != null) {
332                    sourceFilterName = "sourceSlot_" + sourceSlotName;
333                    mCommandStack.append(new AddSourceSlotCommand(sourceFilterName,
334                                                                  sourceSlotName));
335                    sourcePortName = "frame";
336                }
337                if (targetSlotName != null) {
338                    targetFilterName = "targetSlot_" + targetSlotName;
339                    mCommandStack.append(new AddTargetSlotCommand(targetFilterName,
340                                                                  targetSlotName));
341                    targetPortName = "frame";
342                }
343                assertValueNotNull("sourceFilter", sourceFilterName);
344                assertValueNotNull("sourcePort", sourcePortName);
345                assertValueNotNull("targetFilter", targetFilterName);
346                assertValueNotNull("targetPort", targetPortName);
347                // TODO: Should slot connections auto-branch?
348                mCommandStack.append(new ConnectCommand(sourceFilterName,
349                                                        sourcePortName,
350                                                        targetFilterName,
351                                                        targetPortName));
352            }
353
354            private void addVarCommand(Attributes attributes) throws SAXException {
355                String varName = getRequiredAttribute(attributes, "name");
356                Object varValue = getAssignmentValue(attributes);
357                mCommandStack.append(new AddVariableCommand(varName, varValue));
358            }
359
360            private void beginGraph() throws SAXException {
361                if (mInGraph) {
362                    throw new SAXException("Found more than one graph element in XML!");
363                }
364                mInGraph = true;
365            }
366
367            private void endGraph() {
368                mInGraph = false;
369            }
370
371            private void beginFilter(Attributes attributes) throws SAXException {
372                String className = getRequiredAttribute(attributes, "class");
373                mCurFilterName = getRequiredAttribute(attributes, "name");
374                mCommandStack.append(new AllocateFilterCommand(className, mCurFilterName));
375            }
376
377            private void endFilter() {
378                mCurFilterName = null;
379            }
380
381            private void addFilterInput(Attributes attributes) throws SAXException {
382                // Make sure we are in a filter element
383                if (mCurFilterName == null) {
384                    throw new SAXException("Found 'input' element outside of 'filter' "
385                        + "element!");
386                }
387
388                // Get input name and value
389                String inputName = getRequiredAttribute(attributes, "name");
390                Object inputValue = getAssignmentValue(attributes);
391                if (inputValue == null) {
392                    throw new SAXException("No value specified for input '" + inputName + "' "
393                        + "of filter '" + mCurFilterName + "'!");
394                }
395
396                // Push commmand
397                mCommandStack.append(new SetFilterInputCommand(mCurFilterName,
398                                                               inputName,
399                                                               inputValue));
400            }
401
402            private void assertInGraph(String localName) throws SAXException {
403                if (!mInGraph) {
404                    throw new SAXException("Encountered '" + localName + "' element outside of "
405                        + "'graph' element!");
406                }
407            }
408
409            private static Object getAssignmentValue(Attributes attributes) {
410                String strValue = null;
411                if ((strValue = attributes.getValue("stringValue")) != null) {
412                    return strValue;
413                } else if ((strValue = attributes.getValue("booleanValue")) != null) {
414                    return Boolean.parseBoolean(strValue);
415                } else if ((strValue = attributes.getValue("intValue")) != null) {
416                    return Integer.parseInt(strValue);
417                } else if ((strValue = attributes.getValue("floatValue")) != null) {
418                    return Float.parseFloat(strValue);
419                } else if ((strValue = attributes.getValue("floatsValue")) != null) {
420                    String[] floatStrings = TextUtils.split(strValue, ",");
421                    float[] result = new float[floatStrings.length];
422                    for (int i = 0; i < floatStrings.length; ++i) {
423                        result[i] = Float.parseFloat(floatStrings[i]);
424                    }
425                    return result;
426                } else if ((strValue = attributes.getValue("varValue")) != null) {
427                    return new Variable(strValue);
428                } else {
429                    return null;
430                }
431            }
432
433            private static String getRequiredAttribute(Attributes attributes, String name)
434                    throws SAXException {
435                String result = attributes.getValue(name);
436                if (result == null) {
437                    throw new SAXException("Required attribute '" + name + "' not found!");
438                }
439                return result;
440            }
441
442            private static void assertValueNotNull(String valueName, Object value) {
443                if (value == null) {
444                    throw new NullPointerException("Required value '" + value + "' not specified!");
445                }
446            }
447
448        }
449
450        public XmlGraphReader() {
451            mParserFactory = SAXParserFactory.newInstance();
452        }
453
454        public void parseString(String graphString, CommandStack commandStack) throws IOException {
455            try {
456                XMLReader reader = getReaderForCommandStack(commandStack);
457                reader.parse(new InputSource(new StringReader(graphString)));
458            } catch (SAXException e) {
459                throw new IOException("XML parse error during graph parsing!", e);
460            }
461        }
462
463        public void parseInput(InputStream inputStream, CommandStack commandStack)
464                throws IOException {
465            try {
466                XMLReader reader = getReaderForCommandStack(commandStack);
467                reader.parse(new InputSource(inputStream));
468            } catch (SAXException e) {
469                throw new IOException("XML parse error during graph parsing!", e);
470            }
471        }
472
473        private XMLReader getReaderForCommandStack(CommandStack commandStack) throws IOException {
474            try {
475                SAXParser parser = mParserFactory.newSAXParser();
476                XMLReader reader = parser.getXMLReader();
477                GraphDataHandler graphHandler = new GraphDataHandler(commandStack);
478                reader.setContentHandler(graphHandler);
479                return reader;
480            } catch (ParserConfigurationException e) {
481                throw new IOException("Error creating SAXParser for graph parsing!", e);
482            } catch (SAXException e) {
483                throw new IOException("Error creating XMLReader for graph parsing!", e);
484            }
485        }
486    }
487
488    /**
489     * Read an XML graph from a String.
490     *
491     * This function automatically checks each filters' signatures and throws a Runtime Exception
492     * if required ports are unconnected. Use the 3-parameter version to avoid this behavior.
493     *
494     * @param context the MffContext into which to load the graph.
495     * @param xmlSource the graph specified in XML.
496     * @return the FilterGraph instance for the XML source.
497     * @throws IOException if there was an error parsing the source.
498     */
499    public static FilterGraph readXmlGraph(MffContext context, String xmlSource)
500            throws IOException {
501        FilterGraph.Builder builder = getBuilderForXmlString(context, xmlSource);
502        return builder.build();
503    }
504
505    /**
506     * Read an XML sub-graph from a String.
507     *
508     * @param context the MffContext into which to load the graph.
509     * @param xmlSource the graph specified in XML.
510     * @param parentGraph the parent graph.
511     * @return the FilterGraph instance for the XML source.
512     * @throws IOException if there was an error parsing the source.
513     */
514    public static FilterGraph readXmlSubGraph(
515            MffContext context, String xmlSource, FilterGraph parentGraph)
516            throws IOException {
517        FilterGraph.Builder builder = getBuilderForXmlString(context, xmlSource);
518        return builder.buildSubGraph(parentGraph);
519    }
520
521    /**
522     * Read an XML graph from a resource.
523     *
524     * This function automatically checks each filters' signatures and throws a Runtime Exception
525     * if required ports are unconnected. Use the 3-parameter version to avoid this behavior.
526     *
527     * @param context the MffContext into which to load the graph.
528     * @param resourceId the XML resource ID.
529     * @return the FilterGraph instance for the XML source.
530     * @throws IOException if there was an error reading or parsing the resource.
531     */
532    public static FilterGraph readXmlGraphResource(MffContext context, int resourceId)
533            throws IOException {
534        FilterGraph.Builder builder = getBuilderForXmlResource(context, resourceId);
535        return builder.build();
536    }
537
538    /**
539     * Read an XML graph from a resource.
540     *
541     * This function automatically checks each filters' signatures and throws a Runtime Exception
542     * if required ports are unconnected. Use the 3-parameter version to avoid this behavior.
543     *
544     * @param context the MffContext into which to load the graph.
545     * @param resourceId the XML resource ID.
546     * @return the FilterGraph instance for the XML source.
547     * @throws IOException if there was an error reading or parsing the resource.
548     */
549    public static FilterGraph readXmlSubGraphResource(
550            MffContext context, int resourceId, FilterGraph parentGraph)
551            throws IOException {
552        FilterGraph.Builder builder = getBuilderForXmlResource(context, resourceId);
553        return builder.buildSubGraph(parentGraph);
554    }
555
556    private static FilterGraph.Builder getBuilderForXmlString(MffContext context, String source)
557            throws IOException {
558        XmlGraphReader reader = new XmlGraphReader();
559        CommandStack commands = new CommandStack(context);
560        reader.parseString(source, commands);
561        commands.execute();
562        return commands.getBuilder();
563    }
564
565    private static FilterGraph.Builder getBuilderForXmlResource(MffContext context, int resourceId)
566            throws IOException {
567        InputStream inputStream = context.getApplicationContext().getResources()
568                .openRawResource(resourceId);
569        XmlGraphReader reader = new XmlGraphReader();
570        CommandStack commands = new CommandStack(context);
571        reader.parseInput(inputStream, commands);
572        commands.execute();
573        return commands.getBuilder();
574    }
575}
576
577