/*
* Copyright (C) 2009 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 com.android.layoutopt.uix;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import java.io.File;
import java.io.InputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.zip.ZipFile;
import java.util.zip.ZipEntry;
import java.util.Enumeration;
import java.util.List;
import java.util.ArrayList;
import com.android.layoutopt.uix.xml.XmlDocumentBuilder;
import com.android.layoutopt.uix.rules.Rule;
import com.android.layoutopt.uix.rules.GroovyRule;
import com.android.layoutopt.uix.util.IOUtilities;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
/**
* Analysis engine used to discover inefficiencies in Android XML
* layout documents.
*
* Anaylizing an Android XML layout produces a list of explicit messages
* as well as possible solutions.
*/
public class LayoutAnalyzer {
private static final String RULES_PREFIX = "rules/";
private final XmlDocumentBuilder mBuilder = new XmlDocumentBuilder();
private final List<Rule> mRules = new ArrayList<Rule>();
/**
* Creates a new layout analyzer. This constructor takes no argument
* and will use the default options.
*/
public LayoutAnalyzer() {
loadRules();
}
private void loadRules() {
ClassLoader parent = getClass().getClassLoader();
GroovyClassLoader loader = new GroovyClassLoader(parent);
GroovyShell shell = new GroovyShell(loader);
URL jar = getClass().getProtectionDomain().getCodeSource().getLocation();
ZipFile zip = null;
try {
zip = new ZipFile(new File(jar.toURI()));
Enumeration<? extends ZipEntry> entries = zip.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
if (!entry.isDirectory() && entry.getName().startsWith(RULES_PREFIX)) {
loadRule(shell, entry.getName(), zip.getInputStream(entry));
}
}
} catch (IOException e) {
e.printStackTrace();
} catch (URISyntaxException e) {
e.printStackTrace();
} finally {
try {
if (zip != null) zip.close();
} catch (IOException e) {
// Ignore
}
}
}
private void loadRule(GroovyShell shell, String name, InputStream stream) {
try {
Script script = shell.parse(stream);
mRules.add(new GroovyRule(name, script));
} catch (Exception e) {
System.err.println("Could not load rule " + name + ":");
e.printStackTrace();
} finally {
IOUtilities.close(stream);
}
}
public void addRule(Rule rule) {
if (rule == null) {
throw new IllegalArgumentException("A rule must be non-null");
}
mRules.add(rule);
}
/**
* Analyzes the specified file.
*
* @param file The file to analyze.
*
* @return A {@link com.android.layoutopt.uix.LayoutAnalysis} which
* cannot be null.
*/
public LayoutAnalysis analyze(File file) {
if (file != null && file.exists()) {
InputStream in = null;
try {
in = new FileInputStream(file);
return analyze(file.getPath(), in);
} catch (FileNotFoundException e) {
// Ignore, cannot happen
} finally {
IOUtilities.close(in);
}
}
return LayoutAnalysis.ERROR;
}
/**
* Analyzes the specified XML stream.
*
* @param stream The stream to analyze.
* @return A {@link com.android.layoutopt.uix.LayoutAnalysis} which
* cannot be null.
*/
public LayoutAnalysis analyze(InputStream stream) {
return analyze("<unknown>", stream);
}
private LayoutAnalysis analyze(String name, InputStream stream) {
try {
Document document = mBuilder.parse(stream);
return analyze(name, document);
} catch (SAXException e) {
// Ignore
} catch (IOException e) {
// Ignore
}
return LayoutAnalysis.ERROR;
}
/**
* Analyzes the specified XML document.
*
* @param content The XML document to analyze.
*
* @return A {@link com.android.layoutopt.uix.LayoutAnalysis} which
* cannot be null.
*/
public LayoutAnalysis analyze(String content) {
return analyze("<unknown>", content);
}
/**
* Analyzes the specified XML document.
*
* @param name The name of the document.
* @param content The XML document to analyze.
*
* @return A {@link com.android.layoutopt.uix.LayoutAnalysis} which
* cannot be null.
*/
public LayoutAnalysis analyze(String name, String content) {
try {
Document document = mBuilder.parse(content);
return analyze(name, document);
} catch (SAXException e) {
// Ignore
} catch (IOException e) {
// Ignore
}
return LayoutAnalysis.ERROR;
}
/**
* Analyzes the specified XML document.
*
* @param document The XML document to analyze.
*
* @return A {@link com.android.layoutopt.uix.LayoutAnalysis} which
* cannot be null.
*/
public LayoutAnalysis analyze(Document document) {
return analyze("<unknown>", document);
}
/**
* Analyzes the specified XML document.
*
* @param name The name of the document.
* @param document The XML document to analyze.
*
* @return A {@link com.android.layoutopt.uix.LayoutAnalysis} which
* cannot be null.
*/
public LayoutAnalysis analyze(String name, Document document) {
LayoutAnalysis analysis = new LayoutAnalysis(name);
try {
Element root = document.getDocumentElement();
analyze(analysis, root);
} finally {
analysis.validate();
}
return analysis;
}
private void analyze(LayoutAnalysis analysis, Node node) {
NodeList list = node.getChildNodes();
int count = list.getLength();
applyRules(analysis, node);
for (int i = 0; i < count; i++) {
Node child = list.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE) {
analyze(analysis, child);
}
}
}
private void applyRules(LayoutAnalysis analysis, Node node) {
analysis.setCurrentNode(node);
for (Rule rule : mRules) {
rule.run(analysis, node);
}
}
}