/* * Copyright (C) 2011 The Android Open Source Project * * 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 androidx.media.filterfw; import android.text.TextUtils; import java.io.InputStream; import java.io.IOException; import java.io.StringReader; import java.util.ArrayList; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.DefaultHandler; /** * A GraphReader allows obtaining filter graphs from XML graph files or strings. */ public class GraphReader { private static interface Command { public void execute(CommandStack stack); } private static class CommandStack { private ArrayList<Command> mCommands = new ArrayList<Command>(); private FilterGraph.Builder mBuilder; private FilterFactory mFactory; private MffContext mContext; public CommandStack(MffContext context) { mContext = context; mBuilder = new FilterGraph.Builder(mContext); mFactory = new FilterFactory(); } public void execute() { for (Command command : mCommands) { command.execute(this); } } public void append(Command command) { mCommands.add(command); } public FilterFactory getFactory() { return mFactory; } public MffContext getContext() { return mContext; } protected FilterGraph.Builder getBuilder() { return mBuilder; } } private static class ImportPackageCommand implements Command { private String mPackageName; public ImportPackageCommand(String packageName) { mPackageName = packageName; } @Override public void execute(CommandStack stack) { try { stack.getFactory().addPackage(mPackageName); } catch (IllegalArgumentException e) { throw new RuntimeException(e.getMessage()); } } } private static class AddLibraryCommand implements Command { private String mLibraryName; public AddLibraryCommand(String libraryName) { mLibraryName = libraryName; } @Override public void execute(CommandStack stack) { FilterFactory.addFilterLibrary(mLibraryName); } } private static class AllocateFilterCommand implements Command { private String mClassName; private String mFilterName; public AllocateFilterCommand(String className, String filterName) { mClassName = className; mFilterName = filterName; } @Override public void execute(CommandStack stack) { Filter filter = null; try { filter = stack.getFactory().createFilterByClassName(mClassName, mFilterName, stack.getContext()); } catch (IllegalArgumentException e) { throw new RuntimeException("Error creating filter " + mFilterName + "!", e); } stack.getBuilder().addFilter(filter); } } private static class AddSourceSlotCommand implements Command { private String mName; private String mSlotName; public AddSourceSlotCommand(String name, String slotName) { mName = name; mSlotName = slotName; } @Override public void execute(CommandStack stack) { stack.getBuilder().addFrameSlotSource(mName, mSlotName); } } private static class AddTargetSlotCommand implements Command { private String mName; private String mSlotName; public AddTargetSlotCommand(String name, String slotName) { mName = name; mSlotName = slotName; } @Override public void execute(CommandStack stack) { stack.getBuilder().addFrameSlotTarget(mName, mSlotName); } } private static class AddVariableCommand implements Command { private String mName; private Object mValue; public AddVariableCommand(String name, Object value) { mName = name; mValue = value; } @Override public void execute(CommandStack stack) { stack.getBuilder().addVariable(mName, mValue); } } private static class SetFilterInputCommand implements Command { private String mFilterName; private String mFilterInput; private Object mValue; public SetFilterInputCommand(String filterName, String input, Object value) { mFilterName = filterName; mFilterInput = input; mValue = value; } @Override public void execute(CommandStack stack) { if (mValue instanceof Variable) { String varName = ((Variable)mValue).name; stack.getBuilder().assignVariableToFilterInput(varName, mFilterName, mFilterInput); } else { stack.getBuilder().assignValueToFilterInput(mValue, mFilterName, mFilterInput); } } } private static class ConnectCommand implements Command { private String mSourceFilter; private String mSourcePort; private String mTargetFilter; private String mTargetPort; public ConnectCommand(String sourceFilter, String sourcePort, String targetFilter, String targetPort) { mSourceFilter = sourceFilter; mSourcePort = sourcePort; mTargetFilter = targetFilter; mTargetPort = targetPort; } @Override public void execute(CommandStack stack) { stack.getBuilder().connect(mSourceFilter, mSourcePort, mTargetFilter, mTargetPort); } } private static class Variable { public String name; public Variable(String name) { this.name = name; } } private static class XmlGraphReader { private SAXParserFactory mParserFactory; private static class GraphDataHandler extends DefaultHandler { private CommandStack mCommandStack; private boolean mInGraph = false; private String mCurFilterName = null; public GraphDataHandler(CommandStack commandStack) { mCommandStack = commandStack; } @Override public void startElement(String uri, String localName, String qName, Attributes attr) throws SAXException { if (localName.equals("graph")) { beginGraph(); } else { assertInGraph(localName); if (localName.equals("import")) { addImportCommand(attr); } else if (localName.equals("library")) { addLibraryCommand(attr); } else if (localName.equals("connect")) { addConnectCommand(attr); } else if (localName.equals("var")) { addVarCommand(attr); } else if (localName.equals("filter")) { beginFilter(attr); } else if (localName.equals("input")) { addFilterInput(attr); } else { throw new SAXException("Unknown XML element '" + localName + "'!"); } } } @Override public void endElement (String uri, String localName, String qName) { if (localName.equals("graph")) { endGraph(); } else if (localName.equals("filter")) { endFilter(); } } private void addImportCommand(Attributes attributes) throws SAXException { String packageName = getRequiredAttribute(attributes, "package"); mCommandStack.append(new ImportPackageCommand(packageName)); } private void addLibraryCommand(Attributes attributes) throws SAXException { String libraryName = getRequiredAttribute(attributes, "name"); mCommandStack.append(new AddLibraryCommand(libraryName)); } private void addConnectCommand(Attributes attributes) { String sourcePortName = null; String sourceFilterName = null; String targetPortName = null; String targetFilterName = null; // check for shorthand: <connect source="filter:port" target="filter:port"/> String sourceTag = attributes.getValue("source"); if (sourceTag != null) { String[] sourceParts = sourceTag.split(":"); if (sourceParts.length == 2) { sourceFilterName = sourceParts[0]; sourcePortName = sourceParts[1]; } else { throw new RuntimeException( "'source' tag needs to have format \"filter:port\"! " + "Alternatively, you may use the form " + "'sourceFilter=\"filter\" sourcePort=\"port\"'."); } } else { sourceFilterName = attributes.getValue("sourceFilter"); sourcePortName = attributes.getValue("sourcePort"); } String targetTag = attributes.getValue("target"); if (targetTag != null) { String[] targetParts = targetTag.split(":"); if (targetParts.length == 2) { targetFilterName = targetParts[0]; targetPortName = targetParts[1]; } else { throw new RuntimeException( "'target' tag needs to have format \"filter:port\"! " + "Alternatively, you may use the form " + "'targetFilter=\"filter\" targetPort=\"port\"'."); } } else { targetFilterName = attributes.getValue("targetFilter"); targetPortName = attributes.getValue("targetPort"); } String sourceSlotName = attributes.getValue("sourceSlot"); String targetSlotName = attributes.getValue("targetSlot"); if (sourceSlotName != null) { sourceFilterName = "sourceSlot_" + sourceSlotName; mCommandStack.append(new AddSourceSlotCommand(sourceFilterName, sourceSlotName)); sourcePortName = "frame"; } if (targetSlotName != null) { targetFilterName = "targetSlot_" + targetSlotName; mCommandStack.append(new AddTargetSlotCommand(targetFilterName, targetSlotName)); targetPortName = "frame"; } assertValueNotNull("sourceFilter", sourceFilterName); assertValueNotNull("sourcePort", sourcePortName); assertValueNotNull("targetFilter", targetFilterName); assertValueNotNull("targetPort", targetPortName); // TODO: Should slot connections auto-branch? mCommandStack.append(new ConnectCommand(sourceFilterName, sourcePortName, targetFilterName, targetPortName)); } private void addVarCommand(Attributes attributes) throws SAXException { String varName = getRequiredAttribute(attributes, "name"); Object varValue = getAssignmentValue(attributes); mCommandStack.append(new AddVariableCommand(varName, varValue)); } private void beginGraph() throws SAXException { if (mInGraph) { throw new SAXException("Found more than one graph element in XML!"); } mInGraph = true; } private void endGraph() { mInGraph = false; } private void beginFilter(Attributes attributes) throws SAXException { String className = getRequiredAttribute(attributes, "class"); mCurFilterName = getRequiredAttribute(attributes, "name"); mCommandStack.append(new AllocateFilterCommand(className, mCurFilterName)); } private void endFilter() { mCurFilterName = null; } private void addFilterInput(Attributes attributes) throws SAXException { // Make sure we are in a filter element if (mCurFilterName == null) { throw new SAXException("Found 'input' element outside of 'filter' " + "element!"); } // Get input name and value String inputName = getRequiredAttribute(attributes, "name"); Object inputValue = getAssignmentValue(attributes); if (inputValue == null) { throw new SAXException("No value specified for input '" + inputName + "' " + "of filter '" + mCurFilterName + "'!"); } // Push commmand mCommandStack.append(new SetFilterInputCommand(mCurFilterName, inputName, inputValue)); } private void assertInGraph(String localName) throws SAXException { if (!mInGraph) { throw new SAXException("Encountered '" + localName + "' element outside of " + "'graph' element!"); } } private static Object getAssignmentValue(Attributes attributes) { String strValue = null; if ((strValue = attributes.getValue("stringValue")) != null) { return strValue; } else if ((strValue = attributes.getValue("booleanValue")) != null) { return Boolean.parseBoolean(strValue); } else if ((strValue = attributes.getValue("intValue")) != null) { return Integer.parseInt(strValue); } else if ((strValue = attributes.getValue("floatValue")) != null) { return Float.parseFloat(strValue); } else if ((strValue = attributes.getValue("floatsValue")) != null) { String[] floatStrings = TextUtils.split(strValue, ","); float[] result = new float[floatStrings.length]; for (int i = 0; i < floatStrings.length; ++i) { result[i] = Float.parseFloat(floatStrings[i]); } return result; } else if ((strValue = attributes.getValue("varValue")) != null) { return new Variable(strValue); } else { return null; } } private static String getRequiredAttribute(Attributes attributes, String name) throws SAXException { String result = attributes.getValue(name); if (result == null) { throw new SAXException("Required attribute '" + name + "' not found!"); } return result; } private static void assertValueNotNull(String valueName, Object value) { if (value == null) { throw new NullPointerException("Required value '" + value + "' not specified!"); } } } public XmlGraphReader() { mParserFactory = SAXParserFactory.newInstance(); } public void parseString(String graphString, CommandStack commandStack) throws IOException { try { XMLReader reader = getReaderForCommandStack(commandStack); reader.parse(new InputSource(new StringReader(graphString))); } catch (SAXException e) { throw new IOException("XML parse error during graph parsing!", e); } } public void parseInput(InputStream inputStream, CommandStack commandStack) throws IOException { try { XMLReader reader = getReaderForCommandStack(commandStack); reader.parse(new InputSource(inputStream)); } catch (SAXException e) { throw new IOException("XML parse error during graph parsing!", e); } } private XMLReader getReaderForCommandStack(CommandStack commandStack) throws IOException { try { SAXParser parser = mParserFactory.newSAXParser(); XMLReader reader = parser.getXMLReader(); GraphDataHandler graphHandler = new GraphDataHandler(commandStack); reader.setContentHandler(graphHandler); return reader; } catch (ParserConfigurationException e) { throw new IOException("Error creating SAXParser for graph parsing!", e); } catch (SAXException e) { throw new IOException("Error creating XMLReader for graph parsing!", e); } } } /** * Read an XML graph from a String. * * This function automatically checks each filters' signatures and throws a Runtime Exception * if required ports are unconnected. Use the 3-parameter version to avoid this behavior. * * @param context the MffContext into which to load the graph. * @param xmlSource the graph specified in XML. * @return the FilterGraph instance for the XML source. * @throws IOException if there was an error parsing the source. */ public static FilterGraph readXmlGraph(MffContext context, String xmlSource) throws IOException { FilterGraph.Builder builder = getBuilderForXmlString(context, xmlSource); return builder.build(); } /** * Read an XML sub-graph from a String. * * @param context the MffContext into which to load the graph. * @param xmlSource the graph specified in XML. * @param parentGraph the parent graph. * @return the FilterGraph instance for the XML source. * @throws IOException if there was an error parsing the source. */ public static FilterGraph readXmlSubGraph( MffContext context, String xmlSource, FilterGraph parentGraph) throws IOException { FilterGraph.Builder builder = getBuilderForXmlString(context, xmlSource); return builder.buildSubGraph(parentGraph); } /** * Read an XML graph from a resource. * * This function automatically checks each filters' signatures and throws a Runtime Exception * if required ports are unconnected. Use the 3-parameter version to avoid this behavior. * * @param context the MffContext into which to load the graph. * @param resourceId the XML resource ID. * @return the FilterGraph instance for the XML source. * @throws IOException if there was an error reading or parsing the resource. */ public static FilterGraph readXmlGraphResource(MffContext context, int resourceId) throws IOException { FilterGraph.Builder builder = getBuilderForXmlResource(context, resourceId); return builder.build(); } /** * Read an XML graph from a resource. * * This function automatically checks each filters' signatures and throws a Runtime Exception * if required ports are unconnected. Use the 3-parameter version to avoid this behavior. * * @param context the MffContext into which to load the graph. * @param resourceId the XML resource ID. * @return the FilterGraph instance for the XML source. * @throws IOException if there was an error reading or parsing the resource. */ public static FilterGraph readXmlSubGraphResource( MffContext context, int resourceId, FilterGraph parentGraph) throws IOException { FilterGraph.Builder builder = getBuilderForXmlResource(context, resourceId); return builder.buildSubGraph(parentGraph); } private static FilterGraph.Builder getBuilderForXmlString(MffContext context, String source) throws IOException { XmlGraphReader reader = new XmlGraphReader(); CommandStack commands = new CommandStack(context); reader.parseString(source, commands); commands.execute(); return commands.getBuilder(); } private static FilterGraph.Builder getBuilderForXmlResource(MffContext context, int resourceId) throws IOException { InputStream inputStream = context.getApplicationContext().getResources() .openRawResource(resourceId); XmlGraphReader reader = new XmlGraphReader(); CommandStack commands = new CommandStack(context); reader.parseInput(inputStream, commands); commands.execute(); return commands.getBuilder(); } }