1 /* 2 * Copyright (C) 2009 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 17 package com.android.layoutopt.uix; 18 19 import org.w3c.dom.Document; 20 import org.w3c.dom.Element; 21 import org.w3c.dom.Node; 22 import org.w3c.dom.NodeList; 23 import org.xml.sax.SAXException; 24 25 import java.io.File; 26 import java.io.InputStream; 27 import java.io.FileInputStream; 28 import java.io.FileNotFoundException; 29 import java.io.IOException; 30 import java.net.URISyntaxException; 31 import java.net.URL; 32 import java.util.zip.ZipFile; 33 import java.util.zip.ZipEntry; 34 import java.util.Enumeration; 35 import java.util.List; 36 import java.util.ArrayList; 37 38 import com.android.layoutopt.uix.xml.XmlDocumentBuilder; 39 import com.android.layoutopt.uix.rules.Rule; 40 import com.android.layoutopt.uix.rules.GroovyRule; 41 import com.android.layoutopt.uix.util.IOUtilities; 42 import groovy.lang.GroovyClassLoader; 43 import groovy.lang.GroovyShell; 44 import groovy.lang.Script; 45 46 /** 47 * Analysis engine used to discover inefficiencies in Android XML 48 * layout documents. 49 * 50 * Anaylizing an Android XML layout produces a list of explicit messages 51 * as well as possible solutions. 52 */ 53 public class LayoutAnalyzer { 54 private static final String RULES_PREFIX = "rules/"; 55 56 private final XmlDocumentBuilder mBuilder = new XmlDocumentBuilder(); 57 private final List<Rule> mRules = new ArrayList<Rule>(); 58 59 /** 60 * Creates a new layout analyzer. This constructor takes no argument 61 * and will use the default options. 62 */ LayoutAnalyzer()63 public LayoutAnalyzer() { 64 loadRules(); 65 } 66 loadRules()67 private void loadRules() { 68 ClassLoader parent = getClass().getClassLoader(); 69 GroovyClassLoader loader = new GroovyClassLoader(parent); 70 GroovyShell shell = new GroovyShell(loader); 71 72 URL jar = getClass().getProtectionDomain().getCodeSource().getLocation(); 73 ZipFile zip = null; 74 try { 75 zip = new ZipFile(new File(jar.toURI())); 76 Enumeration<? extends ZipEntry> entries = zip.entries(); 77 while (entries.hasMoreElements()) { 78 ZipEntry entry = entries.nextElement(); 79 if (!entry.isDirectory() && entry.getName().startsWith(RULES_PREFIX)) { 80 loadRule(shell, entry.getName(), zip.getInputStream(entry)); 81 } 82 } 83 } catch (IOException e) { 84 e.printStackTrace(); 85 } catch (URISyntaxException e) { 86 e.printStackTrace(); 87 } finally { 88 try { 89 if (zip != null) zip.close(); 90 } catch (IOException e) { 91 // Ignore 92 } 93 } 94 } 95 loadRule(GroovyShell shell, String name, InputStream stream)96 private void loadRule(GroovyShell shell, String name, InputStream stream) { 97 try { 98 Script script = shell.parse(stream); 99 mRules.add(new GroovyRule(name, script)); 100 } catch (Exception e) { 101 System.err.println("Could not load rule " + name + ":"); 102 e.printStackTrace(); 103 } finally { 104 IOUtilities.close(stream); 105 } 106 } 107 addRule(Rule rule)108 public void addRule(Rule rule) { 109 if (rule == null) { 110 throw new IllegalArgumentException("A rule must be non-null"); 111 } 112 mRules.add(rule); 113 } 114 115 /** 116 * Analyzes the specified file. 117 * 118 * @param file The file to analyze. 119 * 120 * @return A {@link com.android.layoutopt.uix.LayoutAnalysis} which 121 * cannot be null. 122 */ analyze(File file)123 public LayoutAnalysis analyze(File file) { 124 if (file != null && file.exists()) { 125 InputStream in = null; 126 try { 127 in = new FileInputStream(file); 128 return analyze(file.getPath(), in); 129 } catch (FileNotFoundException e) { 130 // Ignore, cannot happen 131 } finally { 132 IOUtilities.close(in); 133 } 134 } 135 136 return LayoutAnalysis.ERROR; 137 } 138 139 /** 140 * Analyzes the specified XML stream. 141 * 142 * @param stream The stream to analyze. 143 * @return A {@link com.android.layoutopt.uix.LayoutAnalysis} which 144 * cannot be null. 145 */ analyze(InputStream stream)146 public LayoutAnalysis analyze(InputStream stream) { 147 return analyze("<unknown>", stream); 148 } 149 analyze(String name, InputStream stream)150 private LayoutAnalysis analyze(String name, InputStream stream) { 151 try { 152 Document document = mBuilder.parse(stream); 153 return analyze(name, document); 154 } catch (SAXException e) { 155 // Ignore 156 } catch (IOException e) { 157 // Ignore 158 } 159 return LayoutAnalysis.ERROR; 160 } 161 162 /** 163 * Analyzes the specified XML document. 164 * 165 * @param content The XML document to analyze. 166 * 167 * @return A {@link com.android.layoutopt.uix.LayoutAnalysis} which 168 * cannot be null. 169 */ analyze(String content)170 public LayoutAnalysis analyze(String content) { 171 return analyze("<unknown>", content); 172 } 173 174 /** 175 * Analyzes the specified XML document. 176 * 177 * @param name The name of the document. 178 * @param content The XML document to analyze. 179 * 180 * @return A {@link com.android.layoutopt.uix.LayoutAnalysis} which 181 * cannot be null. 182 */ analyze(String name, String content)183 public LayoutAnalysis analyze(String name, String content) { 184 try { 185 Document document = mBuilder.parse(content); 186 return analyze(name, document); 187 } catch (SAXException e) { 188 // Ignore 189 } catch (IOException e) { 190 // Ignore 191 } 192 return LayoutAnalysis.ERROR; 193 } 194 195 /** 196 * Analyzes the specified XML document. 197 * 198 * @param document The XML document to analyze. 199 * 200 * @return A {@link com.android.layoutopt.uix.LayoutAnalysis} which 201 * cannot be null. 202 */ analyze(Document document)203 public LayoutAnalysis analyze(Document document) { 204 return analyze("<unknown>", document); 205 } 206 207 /** 208 * Analyzes the specified XML document. 209 * 210 * @param name The name of the document. 211 * @param document The XML document to analyze. 212 * 213 * @return A {@link com.android.layoutopt.uix.LayoutAnalysis} which 214 * cannot be null. 215 */ analyze(String name, Document document)216 public LayoutAnalysis analyze(String name, Document document) { 217 LayoutAnalysis analysis = new LayoutAnalysis(name); 218 219 try { 220 Element root = document.getDocumentElement(); 221 analyze(analysis, root); 222 } finally { 223 analysis.validate(); 224 } 225 226 return analysis; 227 } 228 analyze(LayoutAnalysis analysis, Node node)229 private void analyze(LayoutAnalysis analysis, Node node) { 230 NodeList list = node.getChildNodes(); 231 int count = list.getLength(); 232 233 applyRules(analysis, node); 234 235 for (int i = 0; i < count; i++) { 236 Node child = list.item(i); 237 if (child.getNodeType() == Node.ELEMENT_NODE) { 238 analyze(analysis, child); 239 } 240 } 241 } 242 applyRules(LayoutAnalysis analysis, Node node)243 private void applyRules(LayoutAnalysis analysis, Node node) { 244 analysis.setCurrentNode(node); 245 for (Rule rule : mRules) { 246 rule.run(analysis, node); 247 } 248 } 249 } 250