1/*
2 * Copyright 2010 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * 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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package com.android.accessibility;
18
19import org.xml.sax.InputSource;
20import org.xml.sax.SAXException;
21import org.xml.sax.XMLReader;
22import org.xml.sax.helpers.XMLReaderFactory;
23
24import java.io.*;
25import java.util.ArrayList;
26import java.util.List;
27import java.util.logging.Logger;
28
29/**
30 * An object that fetches all Android layout files and manages the testing of
31 * the files with the use of the AccessibilityValidationContentHandler. This
32 * object also reports on any errors encountered during the testing.
33 *
34 * @author dtseng@google.com (David Tseng)
35 */
36public class AccessibilityValidator {
37    /** The root path to scan for Android layout files. */
38    private final File mRootFilePath;
39    /** Errors generated by thrown exceptions (and not by validation errors). */
40    private final List<String> mGeneralErrors = new ArrayList<String>();
41    /** A list of files we wish to have tested. */
42    private List<InputSource> mLayoutFiles;
43    /** The total number of validation test errors across all files. */
44    private int mTotalValidationErrors = 0;
45    /** The path to the Android sdk jar. */
46    private final File mAndroidSdkPath;
47
48    /** The object that handles our logging. */
49    private static final Logger sLogger = Logger.getLogger("android.accessibility");
50
51    /**
52     * The entry point to this tool.
53     *
54     * @param 
55     *            path on which to search for layout xml files that need
56     *            validation
57     */
58    public static void main(String[] args) {
59        sLogger.info("AccessibilityValidator");
60        if (args.length == 2) {
61            sLogger.info("Validating classes using android jar for subclasses of ImageView");
62            new AccessibilityValidator(args[0], args[1]).run();
63        } else {
64            sLogger.info("Usage: java AccessibilityValidator <path> <Android jar path>");
65            return;
66        }
67    }
68
69    /**
70     * Constructs an AccessibilityValidator object using the root path and the
71     * android jar path.
72     */
73    public AccessibilityValidator(String rootPath, String androidSdkPath) {
74        mRootFilePath = new File(rootPath);
75        mAndroidSdkPath = new File(androidSdkPath);
76
77        if (!mRootFilePath.exists()) {
78            throw new IllegalArgumentException("Invalid root path specified "
79                    + rootPath);
80        } else if (!mAndroidSdkPath.exists()) {
81            throw new IllegalArgumentException(
82                    "Invalid android sdk path specified " + androidSdkPath);
83        }
84    }
85
86    /**
87     * Performs validation of Android layout files and logs errors that have
88     * been encountered during the validation. Returns true if the validation
89     * passes.
90     */
91    private boolean run() {
92        sLogger.info("Validating files under " + mRootFilePath);
93        mLayoutFiles = findLayoutFiles(mRootFilePath);
94        validateFiles();
95        for (String error : mGeneralErrors) {
96            sLogger.info(error);
97        }
98        sLogger.info("done with validation");
99        return mGeneralErrors.size() == 0;
100    }
101
102    /**
103     * Accumulates a list of files under the path that meet two constraints.
104     * Firstly, it has a containing (parent) directory of "layout". Secondly, it
105     * has an xml extension
106     */
107    private List<InputSource> findLayoutFiles(File directory) {
108        List<InputSource> layoutFiles = new ArrayList<InputSource>();
109
110        for (File file : directory.listFiles()) {
111            // The file is a directory; recurse on the file.
112            if (file.isDirectory()) {
113                List<InputSource> directoryFiles = findLayoutFiles(file);
114                layoutFiles.addAll(directoryFiles);
115                // Does the containing directory and filename meet our
116                // constraints?
117            } else if (directory.getName().toLowerCase().contains("layout")
118                    && file.getName().toLowerCase().endsWith(".xml")) {
119                InputSource addition;
120                try {
121                    addition = new InputSource(new FileReader(file));
122                    // Store this explicitly for logging.
123                    addition.setPublicId(file.toString());
124                    layoutFiles.add(addition);
125                } catch (FileNotFoundException fileNotFoundException) {
126                    mGeneralErrors.add("File not found "
127                            + fileNotFoundException);
128                }
129            }
130        }
131
132        return layoutFiles;
133    }
134
135    /*
136     * Processes a list of files via an AccessibilityValidationContentHandler.
137     * The caller will only be notified of errors via logging.
138     */
139    public void validateFiles() {
140        sLogger.info("Validating " + getLayoutFiles().size());
141        XMLReader reader;
142        try {
143            reader = XMLReaderFactory.createXMLReader();
144        } catch (SAXException saxExp) {
145            mGeneralErrors.add("Error " + saxExp);
146            return;
147        }
148        for (InputSource file : getLayoutFiles()) {
149            try {
150                AccessibilityValidationContentHandler contentHandler
151                = new AccessibilityValidationContentHandler(
152                        file.getPublicId(), mAndroidSdkPath);
153                reader.setContentHandler(contentHandler);
154                reader.parse(file);
155                mTotalValidationErrors += contentHandler.getValidationErrors();
156            } catch (IOException ioExp) {
157                mGeneralErrors.add("Error reading file " + ioExp);
158            } catch (SAXException saxExp) {
159                mGeneralErrors.add("Error " + saxExp);
160            }
161        }
162    }
163
164    /**
165     * Returns the number of general errors (considered caught exceptions).
166     */
167    public List<String> getGeneralErrors() {
168        return mGeneralErrors;
169    }
170
171    /**
172     * Sets the files to be tested.
173     */
174    public void setLayoutFiles(List<InputSource> layoutFiles) {
175        this.mLayoutFiles = layoutFiles;
176    }
177
178    /**
179     * Gets the files to be tested.
180     */
181    public List<InputSource> getLayoutFiles() {
182        return mLayoutFiles;
183    }
184
185    /**
186     * Gets the total number of test validation errors across all files.
187     */
188    public int getTotalValidationErrors() {
189        return mTotalValidationErrors;
190    }
191}
192