LayoutFileParser.java revision 3e3bf43a2e11fb433b43558e2e05255edfa5b6a8
1/* 2 * Copyright (C) 2015 The Android Open Source Project 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * http://www.apache.org/licenses/LICENSE-2.0 7 * Unless required by applicable law or agreed to in writing, software 8 * distributed under the License is distributed on an "AS IS" BASIS, 9 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 * See the License for the specific language governing permissions and 11 * limitations under the License. 12 */ 13 14package android.databinding.tool.store; 15 16import com.google.common.base.Preconditions; 17 18import org.apache.commons.io.FileUtils; 19import org.apache.commons.lang3.StringUtils; 20import org.w3c.dom.Document; 21import org.w3c.dom.NamedNodeMap; 22import org.w3c.dom.Node; 23import org.w3c.dom.NodeList; 24import org.xml.sax.SAXException; 25 26import android.databinding.tool.util.L; 27import android.databinding.tool.util.ParserHelper; 28import android.databinding.tool.util.XmlEditor; 29 30import java.io.File; 31import java.io.IOException; 32import java.net.MalformedURLException; 33import java.net.URISyntaxException; 34import java.net.URL; 35import java.util.ArrayList; 36import java.util.HashMap; 37import java.util.List; 38 39import javax.xml.parsers.DocumentBuilder; 40import javax.xml.parsers.DocumentBuilderFactory; 41import javax.xml.parsers.ParserConfigurationException; 42import javax.xml.xpath.XPath; 43import javax.xml.xpath.XPathConstants; 44import javax.xml.xpath.XPathExpression; 45import javax.xml.xpath.XPathExpressionException; 46import javax.xml.xpath.XPathFactory; 47 48/** 49 * Gets the list of XML files and creates a list of 50 * {@link android.databinding.tool.store.ResourceBundle} that can be persistent or converted to 51 * LayoutBinder. 52 */ 53public class LayoutFileParser { 54 private static final String XPATH_VARIABLE_DEFINITIONS = "/layout/data/variable"; 55 private static final String XPATH_BINDING_ELEMENTS = "/layout/*[name() != 'data' and name() != 'merge'] | /layout/merge/* | //*[include/.. or @*[starts-with(., '@{') and substring(., string-length(.)) = '}']]"; 56 private static final String XPATH_ID_ELEMENTS = "//*[@*[local-name()='id']]"; 57 private static final String XPATH_IMPORT_DEFINITIONS = "/layout/data/import"; 58 private static final String XPATH_MERGE_ELEMENT = "/layout/merge"; 59 private static final String XPATH_BINDING_LAYOUT = "/layout"; 60 private static final String XPATH_MERGE_INCLUDE = "/layout/merge/include"; 61 private static final String XPATH_BINDING_CLASS = "/layout/data/@class"; 62 final String LAYOUT_PREFIX = "@layout/"; 63 64 public ResourceBundle.LayoutFileBundle parseXml(File xml, String pkg, int minSdk) 65 throws ParserConfigurationException, IOException, SAXException, 66 XPathExpressionException { 67 final String xmlNoExtension = ParserHelper.stripExtension(xml.getName()); 68 final String newTag = xml.getParentFile().getName() + '/' + xmlNoExtension; 69 File original = stripFileAndGetOriginal(xml, newTag); 70 if (original == null) { 71 L.d("assuming the file is the original for %s", xml.getAbsoluteFile()); 72 original = xml; 73 } 74 L.d("parsing file %s", xml.getAbsolutePath()); 75 76 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 77 final DocumentBuilder builder = factory.newDocumentBuilder(); 78 final Document doc = builder.parse(original); 79 80 final XPathFactory xPathFactory = XPathFactory.newInstance(); 81 final XPath xPath = xPathFactory.newXPath(); 82 83 if (!isBindingLayout(doc, xPath)) { 84 return null; 85 } 86 87 if (hasMergeInclude(doc, xPath)) { 88 L.e("Data binding does not support include elements as direct children of a " + 89 "merge element: %s", xml.getPath()); 90 return null; 91 } 92 93 List<Node> variableNodes = getVariableNodes(doc, xPath); 94 final List<Node> imports = getImportNodes(doc, xPath); 95 96 ResourceBundle.LayoutFileBundle bundle = new ResourceBundle.LayoutFileBundle( 97 xmlNoExtension, xml.getParentFile().getName(), pkg, isMerge(doc, xPath)); 98 99 L.d("number of variable nodes %d", variableNodes.size()); 100 for (Node item : variableNodes) { 101 L.d("reading variable node %s", item); 102 NamedNodeMap attributes = item.getAttributes(); 103 String variableName = attributes.getNamedItem("name").getNodeValue(); 104 String variableType = attributes.getNamedItem("type").getNodeValue(); 105 L.d("name: %s, type:%s", variableName, variableType); 106 bundle.addVariable(variableName, variableType); 107 } 108 109 L.d("import node count %d", imports.size()); 110 for (Node item : imports) { 111 NamedNodeMap attributes = item.getAttributes(); 112 String type = attributes.getNamedItem("type").getNodeValue(); 113 final Node aliasNode = attributes.getNamedItem("alias"); 114 final String alias; 115 if (aliasNode == null) { 116 final String[] split = StringUtils.split(type, '.'); 117 alias = split[split.length - 1]; 118 } else { 119 alias = aliasNode.getNodeValue(); 120 } 121 bundle.addImport(alias, type); 122 } 123 124 final Node layoutParent = getLayoutParent(doc, xPath); 125 final List<Node> bindingNodes = getBindingNodes(doc, xPath); 126 final HashMap<Node, String> nodeTagMap = new HashMap<Node, String>(); 127 L.d("number of binding nodes %d", bindingNodes.size()); 128 int tagNumber = 0; 129 for (Node parent : bindingNodes) { 130 NamedNodeMap attributes = parent.getAttributes(); 131 String nodeName = parent.getNodeName(); 132 String viewName = null; 133 String includedLayoutName = null; 134 final Node id = attributes.getNamedItem("android:id"); 135 final String tag; 136 final Node originalTag = attributes.getNamedItem("android:tag"); 137 if ("include".equals(nodeName)) { 138 // get the layout attribute 139 final Node includedLayout = attributes.getNamedItem("layout"); 140 Preconditions.checkNotNull(includedLayout, "must include a layout"); 141 final String includeValue = includedLayout.getNodeValue(); 142 Preconditions.checkArgument(includeValue.startsWith(LAYOUT_PREFIX)); 143 // if user is binding something there, there MUST be a layout file to be 144 // generated. 145 String layoutName = includeValue.substring(LAYOUT_PREFIX.length()); 146 includedLayoutName = layoutName; 147 tag = nodeTagMap.get(parent.getParentNode()); 148 } else if ("fragment".equals(nodeName)) { 149 L.e("fragments do not support data binding expressions: %s", xml.getPath()); 150 continue; 151 } else { 152 viewName = getViewName(parent); 153 if (layoutParent == parent.getParentNode()) { 154 tag = newTag + "_" + tagNumber; 155 } else { 156 tag = "binding_" + tagNumber; 157 } 158 tagNumber++; 159 } 160 final ResourceBundle.BindingTargetBundle bindingTargetBundle = 161 bundle.createBindingTarget(id == null ? null : id.getNodeValue(), 162 viewName, true, tag, originalTag == null ? null : originalTag.getNodeValue()); 163 nodeTagMap.put(parent, tag); 164 bindingTargetBundle.setIncludedLayout(includedLayoutName); 165 166 final int attrCount = attributes.getLength(); 167 for (int i = 0; i < attrCount; i ++) { 168 final Node attr = attributes.item(i); 169 String value = attr.getNodeValue(); 170 if (value.charAt(0) == '@' && value.charAt(1) == '{' && 171 value.charAt(value.length() - 1) == '}') { 172 final String strippedValue = value.substring(2, value.length() - 1); 173 bindingTargetBundle.addBinding(attr.getNodeName(), strippedValue); 174 } 175 } 176 } 177 178 final List<Node> idNodes = getNakedIds(doc, xPath); 179 for (Node node : idNodes) { 180 if (!bindingNodes.contains(node) && !"include".equals(node.getNodeName()) && 181 !"fragment".equals(node.getNodeName())) { 182 final Node id = node.getAttributes().getNamedItem("android:id"); 183 final String className = getViewName(node); 184 bundle.createBindingTarget(id.getNodeValue(), className, true, null, null); 185 } 186 } 187 188 bundle.setBindingClass(getBindingClass(doc, xPath, original)); 189 190 return bundle; 191 } 192 193 private boolean isBindingLayout(Document doc, XPath xPath) throws XPathExpressionException { 194 return !get(doc, xPath, XPATH_BINDING_LAYOUT).isEmpty(); 195 } 196 197 private boolean hasMergeInclude(Document doc, XPath xPath) throws XPathExpressionException { 198 return !get(doc, xPath, XPATH_MERGE_INCLUDE).isEmpty(); 199 } 200 201 private Node getLayoutParent(Document doc, XPath xPath) throws XPathExpressionException { 202 if (isMerge(doc, xPath)) { 203 return get(doc, xPath, XPATH_MERGE_ELEMENT).get(0); 204 } else { 205 return get(doc, xPath, XPATH_BINDING_LAYOUT).get(0); 206 } 207 } 208 209 private String getBindingClass(Document doc, XPath xPath, File file) 210 throws XPathExpressionException { 211 List<Node> nodes = get(doc, xPath, XPATH_BINDING_CLASS); 212 if (nodes.isEmpty()) { 213 return null; 214 } 215 if (nodes.size() > 1) { 216 L.e("More than one binding class declared in %s", file.getAbsolutePath()); 217 } 218 return nodes.get(0).getNodeValue(); 219 } 220 221 private List<Node> getBindingNodes(Document doc, XPath xPath) throws XPathExpressionException { 222 return get(doc, xPath, XPATH_BINDING_ELEMENTS); 223 } 224 225 private List<Node> getVariableNodes(Document doc, XPath xPath) throws XPathExpressionException { 226 return get(doc, xPath, XPATH_VARIABLE_DEFINITIONS); 227 } 228 229 private List<Node> getImportNodes(Document doc, XPath xPath) throws XPathExpressionException { 230 return get(doc, xPath, XPATH_IMPORT_DEFINITIONS); 231 } 232 233 private List<Node> getNakedIds(Document doc, XPath xPath) throws XPathExpressionException { 234 return get(doc, xPath, XPATH_ID_ELEMENTS); 235 } 236 237 private boolean isMerge(Document doc, XPath xPath) throws XPathExpressionException { 238 return !get(doc, xPath, XPATH_MERGE_ELEMENT).isEmpty(); 239 } 240 241 private List<Node> get(Document doc, XPath xPath, String pattern) 242 throws XPathExpressionException { 243 final XPathExpression expr = xPath.compile(pattern); 244 return toList((NodeList) expr.evaluate(doc, XPathConstants.NODESET)); 245 } 246 247 private List<Node> toList(NodeList nodeList) { 248 List<Node> result = new ArrayList<Node>(); 249 for (int i = 0; i < nodeList.getLength(); i ++) { 250 result.add(nodeList.item(i)); 251 } 252 return result; 253 } 254 255 private String getViewName(Node viewNode) { 256 String viewName = viewNode.getNodeName(); 257 if ("view".equals(viewName)) { 258 Node classNode = viewNode.getAttributes().getNamedItem("class"); 259 if (classNode == null) { 260 L.e("No class attribute for 'view' node"); 261 } else { 262 viewName = classNode.getNodeValue(); 263 } 264 } 265 return viewName; 266 } 267 268 private void stripBindingTags(File xml, String newTag) throws IOException { 269 String res = XmlEditor.strip(xml, newTag); 270 if (res != null) { 271 L.d("file %s has changed, overwriting %s", xml.getName(), xml.getAbsolutePath()); 272 FileUtils.writeStringToFile(xml, res); 273 } 274 } 275 276 private File stripFileAndGetOriginal(File xml, String binderId) 277 throws ParserConfigurationException, IOException, SAXException, 278 XPathExpressionException { 279 L.d("parsing resource file %s", xml.getAbsolutePath()); 280 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 281 DocumentBuilder builder = factory.newDocumentBuilder(); 282 Document doc = builder.parse(xml); 283 XPathFactory xPathFactory = XPathFactory.newInstance(); 284 XPath xPath = xPathFactory.newXPath(); 285 final XPathExpression commentElementExpr = xPath 286 .compile("//comment()[starts-with(., \" From: file:\")][last()]"); 287 final NodeList commentElementNodes = (NodeList) commentElementExpr 288 .evaluate(doc, XPathConstants.NODESET); 289 L.d("comment element nodes count %s", commentElementNodes.getLength()); 290 if (commentElementNodes.getLength() == 0) { 291 L.d("cannot find comment element to find the actual file"); 292 return null; 293 } 294 final Node first = commentElementNodes.item(0); 295 String actualFilePath = first.getNodeValue().substring(" From:".length()).trim(); 296 L.d("actual file to parse: %s", actualFilePath); 297 File actualFile = urlToFile(new java.net.URL(actualFilePath)); 298 if (!actualFile.canRead()) { 299 L.d("cannot find original, skipping. %s", actualFile.getAbsolutePath()); 300 return null; 301 } 302 303 // now if file has any binding expressions, find and delete them 304 // TODO we should rely on namespace to avoid parsing file twice 305 boolean changed = isBindingLayout(doc, xPath); 306 if (changed) { 307 stripBindingTags(xml, binderId); 308 } 309 return actualFile; 310 } 311 312 public static File urlToFile(String url) throws MalformedURLException { 313 return urlToFile(new URL(url)); 314 } 315 316 public static File urlToFile(URL url) throws MalformedURLException { 317 try { 318 return new File(url.toURI()); 319 } 320 catch (IllegalArgumentException e) { 321 MalformedURLException ex = new MalformedURLException(e.getLocalizedMessage()); 322 ex.initCause(e); 323 throw ex; 324 } 325 catch (URISyntaxException e) { 326 return new File(url.getPath()); 327 } 328 } 329} 330