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