1663744c839163924037ff1e8c818f8b540675170David Tseng/*
2663744c839163924037ff1e8c818f8b540675170David Tseng * Copyright 2010 Google Inc.
3663744c839163924037ff1e8c818f8b540675170David Tseng *
4663744c839163924037ff1e8c818f8b540675170David Tseng * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5663744c839163924037ff1e8c818f8b540675170David Tseng * use this file except in compliance with the License. You may obtain a copy of
6663744c839163924037ff1e8c818f8b540675170David Tseng * the License at
7663744c839163924037ff1e8c818f8b540675170David Tseng *
8663744c839163924037ff1e8c818f8b540675170David Tseng * http://www.apache.org/licenses/LICENSE-2.0
9663744c839163924037ff1e8c818f8b540675170David Tseng *
10663744c839163924037ff1e8c818f8b540675170David Tseng * Unless required by applicable law or agreed to in writing, software
11663744c839163924037ff1e8c818f8b540675170David Tseng * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12663744c839163924037ff1e8c818f8b540675170David Tseng * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13663744c839163924037ff1e8c818f8b540675170David Tseng * License for the specific language governing permissions and limitations under
14663744c839163924037ff1e8c818f8b540675170David Tseng * the License.
15663744c839163924037ff1e8c818f8b540675170David Tseng */
16663744c839163924037ff1e8c818f8b540675170David Tseng
17663744c839163924037ff1e8c818f8b540675170David Tsengpackage com.android.accessibility;
18663744c839163924037ff1e8c818f8b540675170David Tseng
19663744c839163924037ff1e8c818f8b540675170David Tsengimport org.xml.sax.InputSource;
20663744c839163924037ff1e8c818f8b540675170David Tsengimport org.xml.sax.SAXException;
21663744c839163924037ff1e8c818f8b540675170David Tsengimport org.xml.sax.XMLReader;
22663744c839163924037ff1e8c818f8b540675170David Tsengimport org.xml.sax.helpers.XMLReaderFactory;
23663744c839163924037ff1e8c818f8b540675170David Tseng
24663744c839163924037ff1e8c818f8b540675170David Tsengimport java.io.*;
25663744c839163924037ff1e8c818f8b540675170David Tsengimport java.util.ArrayList;
26663744c839163924037ff1e8c818f8b540675170David Tsengimport java.util.List;
27663744c839163924037ff1e8c818f8b540675170David Tsengimport java.util.logging.Logger;
28663744c839163924037ff1e8c818f8b540675170David Tseng
29663744c839163924037ff1e8c818f8b540675170David Tseng/**
30663744c839163924037ff1e8c818f8b540675170David Tseng * An object that fetches all Android layout files and manages the testing of
31663744c839163924037ff1e8c818f8b540675170David Tseng * the files with the use of the AccessibilityValidationContentHandler. This
32663744c839163924037ff1e8c818f8b540675170David Tseng * object also reports on any errors encountered during the testing.
33663744c839163924037ff1e8c818f8b540675170David Tseng *
34663744c839163924037ff1e8c818f8b540675170David Tseng * @author dtseng@google.com (David Tseng)
35663744c839163924037ff1e8c818f8b540675170David Tseng */
36663744c839163924037ff1e8c818f8b540675170David Tsengpublic class AccessibilityValidator {
37663744c839163924037ff1e8c818f8b540675170David Tseng    /** The root path to scan for Android layout files. */
38663744c839163924037ff1e8c818f8b540675170David Tseng    private final File mRootFilePath;
39663744c839163924037ff1e8c818f8b540675170David Tseng    /** Errors generated by thrown exceptions (and not by validation errors). */
40663744c839163924037ff1e8c818f8b540675170David Tseng    private final List<String> mGeneralErrors = new ArrayList<String>();
41663744c839163924037ff1e8c818f8b540675170David Tseng    /** A list of files we wish to have tested. */
42663744c839163924037ff1e8c818f8b540675170David Tseng    private List<InputSource> mLayoutFiles;
43663744c839163924037ff1e8c818f8b540675170David Tseng    /** The total number of validation test errors across all files. */
44663744c839163924037ff1e8c818f8b540675170David Tseng    private int mTotalValidationErrors = 0;
45663744c839163924037ff1e8c818f8b540675170David Tseng    /** The path to the Android sdk jar. */
46663744c839163924037ff1e8c818f8b540675170David Tseng    private final File mAndroidSdkPath;
47663744c839163924037ff1e8c818f8b540675170David Tseng
48663744c839163924037ff1e8c818f8b540675170David Tseng    /** The object that handles our logging. */
49663744c839163924037ff1e8c818f8b540675170David Tseng    private static final Logger sLogger = Logger.getLogger("android.accessibility");
50663744c839163924037ff1e8c818f8b540675170David Tseng
51663744c839163924037ff1e8c818f8b540675170David Tseng    /**
52663744c839163924037ff1e8c818f8b540675170David Tseng     * The entry point to this tool.
53663744c839163924037ff1e8c818f8b540675170David Tseng     *
54663744c839163924037ff1e8c818f8b540675170David Tseng     * @param 
55663744c839163924037ff1e8c818f8b540675170David Tseng     *            path on which to search for layout xml files that need
56663744c839163924037ff1e8c818f8b540675170David Tseng     *            validation
57663744c839163924037ff1e8c818f8b540675170David Tseng     */
58663744c839163924037ff1e8c818f8b540675170David Tseng    public static void main(String[] args) {
59663744c839163924037ff1e8c818f8b540675170David Tseng        sLogger.info("AccessibilityValidator");
60663744c839163924037ff1e8c818f8b540675170David Tseng        if (args.length == 2) {
61663744c839163924037ff1e8c818f8b540675170David Tseng            sLogger.info("Validating classes using android jar for subclasses of ImageView");
62663744c839163924037ff1e8c818f8b540675170David Tseng            new AccessibilityValidator(args[0], args[1]).run();
63663744c839163924037ff1e8c818f8b540675170David Tseng        } else {
64663744c839163924037ff1e8c818f8b540675170David Tseng            sLogger.info("Usage: java AccessibilityValidator <path> <Android jar path>");
65663744c839163924037ff1e8c818f8b540675170David Tseng            return;
66663744c839163924037ff1e8c818f8b540675170David Tseng        }
67663744c839163924037ff1e8c818f8b540675170David Tseng    }
68663744c839163924037ff1e8c818f8b540675170David Tseng
69663744c839163924037ff1e8c818f8b540675170David Tseng    /**
70663744c839163924037ff1e8c818f8b540675170David Tseng     * Constructs an AccessibilityValidator object using the root path and the
71663744c839163924037ff1e8c818f8b540675170David Tseng     * android jar path.
72663744c839163924037ff1e8c818f8b540675170David Tseng     */
73663744c839163924037ff1e8c818f8b540675170David Tseng    public AccessibilityValidator(String rootPath, String androidSdkPath) {
74663744c839163924037ff1e8c818f8b540675170David Tseng        mRootFilePath = new File(rootPath);
75663744c839163924037ff1e8c818f8b540675170David Tseng        mAndroidSdkPath = new File(androidSdkPath);
76663744c839163924037ff1e8c818f8b540675170David Tseng
77663744c839163924037ff1e8c818f8b540675170David Tseng        if (!mRootFilePath.exists()) {
78663744c839163924037ff1e8c818f8b540675170David Tseng            throw new IllegalArgumentException("Invalid root path specified "
79663744c839163924037ff1e8c818f8b540675170David Tseng                    + rootPath);
80663744c839163924037ff1e8c818f8b540675170David Tseng        } else if (!mAndroidSdkPath.exists()) {
81663744c839163924037ff1e8c818f8b540675170David Tseng            throw new IllegalArgumentException(
82663744c839163924037ff1e8c818f8b540675170David Tseng                    "Invalid android sdk path specified " + androidSdkPath);
83663744c839163924037ff1e8c818f8b540675170David Tseng        }
84663744c839163924037ff1e8c818f8b540675170David Tseng    }
85663744c839163924037ff1e8c818f8b540675170David Tseng
86663744c839163924037ff1e8c818f8b540675170David Tseng    /**
87663744c839163924037ff1e8c818f8b540675170David Tseng     * Performs validation of Android layout files and logs errors that have
88663744c839163924037ff1e8c818f8b540675170David Tseng     * been encountered during the validation. Returns true if the validation
89663744c839163924037ff1e8c818f8b540675170David Tseng     * passes.
90663744c839163924037ff1e8c818f8b540675170David Tseng     */
91663744c839163924037ff1e8c818f8b540675170David Tseng    private boolean run() {
92663744c839163924037ff1e8c818f8b540675170David Tseng        sLogger.info("Validating files under " + mRootFilePath);
93663744c839163924037ff1e8c818f8b540675170David Tseng        mLayoutFiles = findLayoutFiles(mRootFilePath);
94663744c839163924037ff1e8c818f8b540675170David Tseng        validateFiles();
95663744c839163924037ff1e8c818f8b540675170David Tseng        for (String error : mGeneralErrors) {
96663744c839163924037ff1e8c818f8b540675170David Tseng            sLogger.info(error);
97663744c839163924037ff1e8c818f8b540675170David Tseng        }
98663744c839163924037ff1e8c818f8b540675170David Tseng        sLogger.info("done with validation");
99663744c839163924037ff1e8c818f8b540675170David Tseng        return mGeneralErrors.size() == 0;
100663744c839163924037ff1e8c818f8b540675170David Tseng    }
101663744c839163924037ff1e8c818f8b540675170David Tseng
102663744c839163924037ff1e8c818f8b540675170David Tseng    /**
103663744c839163924037ff1e8c818f8b540675170David Tseng     * Accumulates a list of files under the path that meet two constraints.
104663744c839163924037ff1e8c818f8b540675170David Tseng     * Firstly, it has a containing (parent) directory of "layout". Secondly, it
105663744c839163924037ff1e8c818f8b540675170David Tseng     * has an xml extension
106663744c839163924037ff1e8c818f8b540675170David Tseng     */
107663744c839163924037ff1e8c818f8b540675170David Tseng    private List<InputSource> findLayoutFiles(File directory) {
108663744c839163924037ff1e8c818f8b540675170David Tseng        List<InputSource> layoutFiles = new ArrayList<InputSource>();
109663744c839163924037ff1e8c818f8b540675170David Tseng
110663744c839163924037ff1e8c818f8b540675170David Tseng        for (File file : directory.listFiles()) {
111663744c839163924037ff1e8c818f8b540675170David Tseng            // The file is a directory; recurse on the file.
112663744c839163924037ff1e8c818f8b540675170David Tseng            if (file.isDirectory()) {
113663744c839163924037ff1e8c818f8b540675170David Tseng                List<InputSource> directoryFiles = findLayoutFiles(file);
114663744c839163924037ff1e8c818f8b540675170David Tseng                layoutFiles.addAll(directoryFiles);
115663744c839163924037ff1e8c818f8b540675170David Tseng                // Does the containing directory and filename meet our
116663744c839163924037ff1e8c818f8b540675170David Tseng                // constraints?
117663744c839163924037ff1e8c818f8b540675170David Tseng            } else if (directory.getName().toLowerCase().contains("layout")
118663744c839163924037ff1e8c818f8b540675170David Tseng                    && file.getName().toLowerCase().endsWith(".xml")) {
119663744c839163924037ff1e8c818f8b540675170David Tseng                InputSource addition;
120663744c839163924037ff1e8c818f8b540675170David Tseng                try {
121663744c839163924037ff1e8c818f8b540675170David Tseng                    addition = new InputSource(new FileReader(file));
122663744c839163924037ff1e8c818f8b540675170David Tseng                    // Store this explicitly for logging.
123663744c839163924037ff1e8c818f8b540675170David Tseng                    addition.setPublicId(file.toString());
124663744c839163924037ff1e8c818f8b540675170David Tseng                    layoutFiles.add(addition);
125663744c839163924037ff1e8c818f8b540675170David Tseng                } catch (FileNotFoundException fileNotFoundException) {
126663744c839163924037ff1e8c818f8b540675170David Tseng                    mGeneralErrors.add("File not found "
127663744c839163924037ff1e8c818f8b540675170David Tseng                            + fileNotFoundException);
128663744c839163924037ff1e8c818f8b540675170David Tseng                }
129663744c839163924037ff1e8c818f8b540675170David Tseng            }
130663744c839163924037ff1e8c818f8b540675170David Tseng        }
131663744c839163924037ff1e8c818f8b540675170David Tseng
132663744c839163924037ff1e8c818f8b540675170David Tseng        return layoutFiles;
133663744c839163924037ff1e8c818f8b540675170David Tseng    }
134663744c839163924037ff1e8c818f8b540675170David Tseng
135663744c839163924037ff1e8c818f8b540675170David Tseng    /*
136663744c839163924037ff1e8c818f8b540675170David Tseng     * Processes a list of files via an AccessibilityValidationContentHandler.
137663744c839163924037ff1e8c818f8b540675170David Tseng     * The caller will only be notified of errors via logging.
138663744c839163924037ff1e8c818f8b540675170David Tseng     */
139663744c839163924037ff1e8c818f8b540675170David Tseng    public void validateFiles() {
140663744c839163924037ff1e8c818f8b540675170David Tseng        sLogger.info("Validating " + getLayoutFiles().size());
141663744c839163924037ff1e8c818f8b540675170David Tseng        XMLReader reader;
142663744c839163924037ff1e8c818f8b540675170David Tseng        try {
143663744c839163924037ff1e8c818f8b540675170David Tseng            reader = XMLReaderFactory.createXMLReader();
144663744c839163924037ff1e8c818f8b540675170David Tseng        } catch (SAXException saxExp) {
145663744c839163924037ff1e8c818f8b540675170David Tseng            mGeneralErrors.add("Error " + saxExp);
146663744c839163924037ff1e8c818f8b540675170David Tseng            return;
147663744c839163924037ff1e8c818f8b540675170David Tseng        }
148663744c839163924037ff1e8c818f8b540675170David Tseng        for (InputSource file : getLayoutFiles()) {
149663744c839163924037ff1e8c818f8b540675170David Tseng            try {
150663744c839163924037ff1e8c818f8b540675170David Tseng                AccessibilityValidationContentHandler contentHandler
151663744c839163924037ff1e8c818f8b540675170David Tseng                = new AccessibilityValidationContentHandler(
152663744c839163924037ff1e8c818f8b540675170David Tseng                        file.getPublicId(), mAndroidSdkPath);
153663744c839163924037ff1e8c818f8b540675170David Tseng                reader.setContentHandler(contentHandler);
154663744c839163924037ff1e8c818f8b540675170David Tseng                reader.parse(file);
155663744c839163924037ff1e8c818f8b540675170David Tseng                mTotalValidationErrors += contentHandler.getValidationErrors();
156663744c839163924037ff1e8c818f8b540675170David Tseng            } catch (IOException ioExp) {
157663744c839163924037ff1e8c818f8b540675170David Tseng                mGeneralErrors.add("Error reading file " + ioExp);
158663744c839163924037ff1e8c818f8b540675170David Tseng            } catch (SAXException saxExp) {
159663744c839163924037ff1e8c818f8b540675170David Tseng                mGeneralErrors.add("Error " + saxExp);
160663744c839163924037ff1e8c818f8b540675170David Tseng            }
161663744c839163924037ff1e8c818f8b540675170David Tseng        }
162663744c839163924037ff1e8c818f8b540675170David Tseng    }
163663744c839163924037ff1e8c818f8b540675170David Tseng
164663744c839163924037ff1e8c818f8b540675170David Tseng    /**
165663744c839163924037ff1e8c818f8b540675170David Tseng     * Returns the number of general errors (considered caught exceptions).
166663744c839163924037ff1e8c818f8b540675170David Tseng     */
167663744c839163924037ff1e8c818f8b540675170David Tseng    public List<String> getGeneralErrors() {
168663744c839163924037ff1e8c818f8b540675170David Tseng        return mGeneralErrors;
169663744c839163924037ff1e8c818f8b540675170David Tseng    }
170663744c839163924037ff1e8c818f8b540675170David Tseng
171663744c839163924037ff1e8c818f8b540675170David Tseng    /**
172663744c839163924037ff1e8c818f8b540675170David Tseng     * Sets the files to be tested.
173663744c839163924037ff1e8c818f8b540675170David Tseng     */
174663744c839163924037ff1e8c818f8b540675170David Tseng    public void setLayoutFiles(List<InputSource> layoutFiles) {
175663744c839163924037ff1e8c818f8b540675170David Tseng        this.mLayoutFiles = layoutFiles;
176663744c839163924037ff1e8c818f8b540675170David Tseng    }
177663744c839163924037ff1e8c818f8b540675170David Tseng
178663744c839163924037ff1e8c818f8b540675170David Tseng    /**
179663744c839163924037ff1e8c818f8b540675170David Tseng     * Gets the files to be tested.
180663744c839163924037ff1e8c818f8b540675170David Tseng     */
181663744c839163924037ff1e8c818f8b540675170David Tseng    public List<InputSource> getLayoutFiles() {
182663744c839163924037ff1e8c818f8b540675170David Tseng        return mLayoutFiles;
183663744c839163924037ff1e8c818f8b540675170David Tseng    }
184663744c839163924037ff1e8c818f8b540675170David Tseng
185663744c839163924037ff1e8c818f8b540675170David Tseng    /**
186663744c839163924037ff1e8c818f8b540675170David Tseng     * Gets the total number of test validation errors across all files.
187663744c839163924037ff1e8c818f8b540675170David Tseng     */
188663744c839163924037ff1e8c818f8b540675170David Tseng    public int getTotalValidationErrors() {
189663744c839163924037ff1e8c818f8b540675170David Tseng        return mTotalValidationErrors;
190663744c839163924037ff1e8c818f8b540675170David Tseng    }
191663744c839163924037ff1e8c818f8b540675170David Tseng}
192