/AccessCheck/src/com/android/accessibility/AccessibilityValidationContentHandler.java

http://eyes-free.googlecode.com/ · Java · 214 lines · 101 code · 21 blank · 92 comment · 16 complexity · e319509ca5e71ffe61360f684af2abf8 MD5 · raw file

  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. package com.android.accessibility;
  17. import org.xml.sax.Attributes;
  18. import org.xml.sax.Locator;
  19. import org.xml.sax.helpers.DefaultHandler;
  20. import java.io.File;
  21. import java.net.MalformedURLException;
  22. import java.net.URL;
  23. import java.net.URLClassLoader;
  24. import java.util.ArrayList;
  25. import java.util.HashSet;
  26. import java.util.List;
  27. import java.util.Set;
  28. import java.util.logging.Logger;
  29. /**
  30. * An object that handles Android xml layout files in conjunction with an
  31. * XMLParser for the purpose of testing for accessibility based on the following
  32. * rule:
  33. * <p>
  34. * If the Element tag is ImageView (or a subclass of ImageView), then the tag
  35. * must contain a contentDescription attribute.
  36. * <p>
  37. * This class also has logic to ascertain the subclasses of ImageView and thus
  38. * requires the path to an Android sdk jar. The subclasses are saved for
  39. * application of the above rule when a new XML document tag needs processing.
  40. *
  41. * @author dtseng@google.com (David Tseng)
  42. */
  43. public class AccessibilityValidationContentHandler extends DefaultHandler {
  44. /** Used to obtain line information within the XML file. */
  45. private Locator mLocator;
  46. /** The location of the file we are handling. */
  47. private final String mPath;
  48. /** The total number of errors within the current file. */
  49. private int mValidationErrors = 0;
  50. /**
  51. * Element tags we have seen before and determined not to be
  52. * subclasses of ImageView.
  53. */
  54. private final Set<String> mExclusionList = new HashSet<String>();
  55. /** The path to the Android sdk jar file. */
  56. private final File mAndroidSdkPath;
  57. /**
  58. * The ImageView class stored for easy comparison while handling content. It
  59. * gets initialized in the {@link AccessibilityValidationHandler}
  60. * constructor if not already done so.
  61. */
  62. private static Class<?> sImageViewElement;
  63. /**
  64. * A class loader properly initialized and reusable across files. It gets
  65. * initialized in the {@link AccessibilityValidationHandler} constructor if
  66. * not already done so.
  67. */
  68. private static ClassLoader sValidationClassLoader;
  69. /** Attributes we test existence for (for example, contentDescription). */
  70. private static final HashSet<String> sExpectedAttributes =
  71. new HashSet<String>();
  72. /** The object that handles our logging. */
  73. private static final Logger sLogger = Logger.getLogger("android.accessibility");
  74. /**
  75. * Construct an AccessibilityValidationContentHandler object with the file
  76. * on which validation occurs and a path to the Android sdk jar. Then,
  77. * initialize the class members if not previously done so.
  78. *
  79. * @throws IllegalArgumentException
  80. * when given an invalid Android sdk path or when unable to
  81. * locate {@link ImageView} class.
  82. */
  83. public AccessibilityValidationContentHandler(String fullyQualifiedPath,
  84. File androidSdkPath) throws IllegalArgumentException {
  85. mPath = fullyQualifiedPath;
  86. mAndroidSdkPath = androidSdkPath;
  87. initializeAccessibilityValidationContentHandler();
  88. }
  89. /**
  90. * Used to log line numbers of errors in {@link #startElement}.
  91. */
  92. @Override
  93. public void setDocumentLocator(Locator locator) {
  94. mLocator = locator;
  95. }
  96. /**
  97. * For each subclass of ImageView, test for existence of the specified
  98. * attributes.
  99. */
  100. @Override
  101. public void startElement(String uri, String localName, String qName,
  102. Attributes atts) {
  103. Class<?> potentialClass;
  104. String classPath = "android.widget." + localName;
  105. try {
  106. potentialClass = sValidationClassLoader.loadClass(classPath);
  107. } catch (ClassNotFoundException cnfException) {
  108. return; // do nothing as the class doesn't exist.
  109. }
  110. // if we already determined this class path isn't a subclass of
  111. // ImageView, skip it.
  112. // Otherwise, check to see if it is a subclass.
  113. if (mExclusionList.contains(classPath)) {
  114. return;
  115. } else if (!sImageViewElement.isAssignableFrom(potentialClass)) {
  116. mExclusionList.add(classPath);
  117. return;
  118. }
  119. boolean hasAttribute = false;
  120. StringBuilder extendedOutput = new StringBuilder();
  121. for (int i = 0; i < atts.getLength(); i++) {
  122. String currentAttribute = atts.getLocalName(i).toLowerCase();
  123. if (sExpectedAttributes.contains(currentAttribute)) {
  124. hasAttribute = true;
  125. break;
  126. } else if (currentAttribute.equals("id")) {
  127. extendedOutput.append("|id=" + currentAttribute);
  128. } else if (currentAttribute.equals("src")) {
  129. extendedOutput.append("|src=" + atts.getValue(i));
  130. }
  131. }
  132. if (!hasAttribute) {
  133. if (getValidationErrors() == 0) {
  134. sLogger.info(mPath);
  135. }
  136. sLogger.info(String.format("ln: %s. Error in %s%s tag.",
  137. mLocator.getLineNumber(), localName, extendedOutput));
  138. mValidationErrors++;
  139. }
  140. }
  141. /**
  142. * Returns the total number of errors encountered in this file.
  143. */
  144. public int getValidationErrors() {
  145. return mValidationErrors;
  146. }
  147. /**
  148. * Set the class loader and ImageView class objects that will be used during
  149. * the startElement validation logic. The class loader encompasses the class
  150. * paths provided.
  151. *
  152. * @throws ClassNotFoundException
  153. * when the ImageView Class object could not be found within the
  154. * provided class loader.
  155. */
  156. public static void setClassLoaderAndBaseClass(URL[] urlSearchPaths)
  157. throws ClassNotFoundException {
  158. sValidationClassLoader = new URLClassLoader(urlSearchPaths);
  159. sImageViewElement =
  160. sValidationClassLoader.loadClass("android.widget.ImageView");
  161. }
  162. /**
  163. * Adds an attribute that will be tested for existence in
  164. * {@link #startElement}. The search will always be case-insensitive.
  165. */
  166. private static void addExpectedAttribute(String attribute) {
  167. sExpectedAttributes.add(attribute.toLowerCase());
  168. }
  169. /**
  170. * Initializes the class loader and {@link ImageView} Class objects.
  171. *
  172. * @throws IllegalArgumentException
  173. * when either an invalid path is provided or ImageView cannot
  174. * be found in the classpaths.
  175. */
  176. private void initializeAccessibilityValidationContentHandler()
  177. throws IllegalArgumentException {
  178. if (sValidationClassLoader != null && sImageViewElement != null) {
  179. return; // These objects are already initialized.
  180. }
  181. try {
  182. setClassLoaderAndBaseClass(new URL[] { mAndroidSdkPath.toURL() });
  183. } catch (MalformedURLException mUException) {
  184. throw new IllegalArgumentException("invalid android sdk path",
  185. mUException);
  186. } catch (ClassNotFoundException cnfException) {
  187. throw new IllegalArgumentException(
  188. "Unable to find ImageView class.", cnfException);
  189. }
  190. // Add all of the expected attributes.
  191. addExpectedAttribute("contentDescription");
  192. }
  193. }