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