1 /* 2 * Copyright (C) 2017 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.tradefed.config; 18 19 import com.android.tradefed.log.LogUtil.CLog; 20 import com.android.tradefed.util.FileUtil; 21 import com.android.tradefed.util.MultiMap; 22 23 import org.kxml2.io.KXmlSerializer; 24 25 import java.io.File; 26 import java.io.IOException; 27 import java.io.PrintWriter; 28 import java.lang.reflect.Field; 29 import java.lang.reflect.InvocationTargetException; 30 import java.util.ArrayList; 31 import java.util.Collection; 32 import java.util.HashSet; 33 import java.util.LinkedHashMap; 34 import java.util.LinkedHashSet; 35 import java.util.List; 36 import java.util.Map; 37 import java.util.Map.Entry; 38 import java.util.Set; 39 40 /** Utility functions to handle configuration files. */ 41 public class ConfigurationUtil { 42 43 // Element names used for emitting the configuration XML. 44 public static final String CONFIGURATION_NAME = "configuration"; 45 public static final String OPTION_NAME = "option"; 46 public static final String CLASS_NAME = "class"; 47 public static final String NAME_NAME = "name"; 48 public static final String KEY_NAME = "key"; 49 public static final String VALUE_NAME = "value"; 50 51 /** 52 * Create a serializer to be used to create a new configuration file. 53 * 54 * @param outputXml the XML file to write to 55 * @return a {@link KXmlSerializer} 56 */ createSerializer(File outputXml)57 static KXmlSerializer createSerializer(File outputXml) throws IOException { 58 PrintWriter output = new PrintWriter(outputXml); 59 KXmlSerializer serializer = new KXmlSerializer(); 60 serializer.setOutput(output); 61 serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); 62 serializer.startDocument("UTF-8", null); 63 return serializer; 64 } 65 66 /** 67 * Add a class to the configuration XML dump. 68 * 69 * @param serializer a {@link KXmlSerializer} to create the XML dump 70 * @param classTypeName a {@link String} of the class type's name 71 * @param obj {@link Object} to be added to the XML dump 72 * @param excludeClassFilter list of object configuration type or fully qualified class names to 73 * be excluded from the dump. for example: {@link Configuration#TARGET_PREPARER_TYPE_NAME}. 74 * com.android.tradefed.testtype.StubTest 75 * @param printDeprecatedOptions whether or not to print deprecated options 76 * @param printUnchangedOptions whether or not to print options that haven't been changed 77 */ dumpClassToXml( KXmlSerializer serializer, String classTypeName, Object obj, List<String> excludeClassFilter, boolean printDeprecatedOptions, boolean printUnchangedOptions)78 static void dumpClassToXml( 79 KXmlSerializer serializer, 80 String classTypeName, 81 Object obj, 82 List<String> excludeClassFilter, 83 boolean printDeprecatedOptions, 84 boolean printUnchangedOptions) 85 throws IOException { 86 dumpClassToXml( 87 serializer, 88 classTypeName, 89 obj, 90 false, 91 excludeClassFilter, 92 printDeprecatedOptions, 93 printUnchangedOptions); 94 } 95 96 /** 97 * Add a class to the configuration XML dump. 98 * 99 * @param serializer a {@link KXmlSerializer} to create the XML dump 100 * @param classTypeName a {@link String} of the class type's name 101 * @param obj {@link Object} to be added to the XML dump 102 * @param isGenericObject Whether or not the object is specified as <object> in the xml 103 * @param excludeClassFilter list of object configuration type or fully qualified class names to 104 * be excluded from the dump. for example: {@link Configuration#TARGET_PREPARER_TYPE_NAME}. 105 * com.android.tradefed.testtype.StubTest 106 * @param printDeprecatedOptions whether or not to print deprecated options 107 * @param printUnchangedOptions whether or not to print options that haven't been changed 108 */ dumpClassToXml( KXmlSerializer serializer, String classTypeName, Object obj, boolean isGenericObject, List<String> excludeClassFilter, boolean printDeprecatedOptions, boolean printUnchangedOptions)109 static void dumpClassToXml( 110 KXmlSerializer serializer, 111 String classTypeName, 112 Object obj, 113 boolean isGenericObject, 114 List<String> excludeClassFilter, 115 boolean printDeprecatedOptions, 116 boolean printUnchangedOptions) 117 throws IOException { 118 if (excludeClassFilter.contains(classTypeName)) { 119 return; 120 } 121 if (excludeClassFilter.contains(obj.getClass().getName())) { 122 return; 123 } 124 if (isGenericObject) { 125 serializer.startTag(null, "object"); 126 serializer.attribute(null, "type", classTypeName); 127 serializer.attribute(null, CLASS_NAME, obj.getClass().getName()); 128 dumpOptionsToXml(serializer, obj, printDeprecatedOptions, printUnchangedOptions); 129 serializer.endTag(null, "object"); 130 } else { 131 serializer.startTag(null, classTypeName); 132 serializer.attribute(null, CLASS_NAME, obj.getClass().getName()); 133 dumpOptionsToXml(serializer, obj, printDeprecatedOptions, printUnchangedOptions); 134 serializer.endTag(null, classTypeName); 135 } 136 } 137 138 /** 139 * Add all the options of class to the command XML dump. 140 * 141 * @param serializer a {@link KXmlSerializer} to create the XML dump 142 * @param obj {@link Object} to be added to the XML dump 143 * @param printDeprecatedOptions whether or not to skip the deprecated options 144 * @param printUnchangedOptions whether or not to print options that haven't been changed 145 */ 146 @SuppressWarnings({"rawtypes", "unchecked"}) dumpOptionsToXml( KXmlSerializer serializer, Object obj, boolean printDeprecatedOptions, boolean printUnchangedOptions)147 private static void dumpOptionsToXml( 148 KXmlSerializer serializer, 149 Object obj, 150 boolean printDeprecatedOptions, 151 boolean printUnchangedOptions) 152 throws IOException { 153 Object comparisonBaseObj = null; 154 if (!printUnchangedOptions) { 155 try { 156 comparisonBaseObj = obj.getClass().getDeclaredConstructor().newInstance(); 157 } catch (InstantiationException 158 | IllegalAccessException 159 | InvocationTargetException 160 | NoSuchMethodException e) { 161 throw new RuntimeException(e); 162 } 163 } 164 165 for (Field field : OptionSetter.getOptionFieldsForClass(obj.getClass())) { 166 Option option = field.getAnnotation(Option.class); 167 Deprecated deprecatedAnnotation = field.getAnnotation(Deprecated.class); 168 // If enabled, skip @Deprecated options 169 if (!printDeprecatedOptions && deprecatedAnnotation != null) { 170 continue; 171 } 172 Object fieldVal = OptionSetter.getFieldValue(field, obj); 173 if (fieldVal == null) { 174 continue; 175 } 176 if (comparisonBaseObj != null) { 177 Object compField = OptionSetter.getFieldValue(field, comparisonBaseObj); 178 if (fieldVal.equals(compField)) { 179 continue; 180 } 181 } 182 183 if (fieldVal instanceof Collection) { 184 for (Object entry : (Collection) fieldVal) { 185 dumpOptionToXml(serializer, option.name(), null, entry.toString()); 186 } 187 } else if (fieldVal instanceof Map) { 188 Map map = (Map) fieldVal; 189 for (Object entryObj : map.entrySet()) { 190 Map.Entry entry = (Entry) entryObj; 191 dumpOptionToXml( 192 serializer, 193 option.name(), 194 entry.getKey().toString(), 195 entry.getValue().toString()); 196 } 197 } else if (fieldVal instanceof MultiMap) { 198 MultiMap multimap = (MultiMap) fieldVal; 199 for (Object keyObj : multimap.keySet()) { 200 for (Object valueObj : multimap.get(keyObj)) { 201 dumpOptionToXml( 202 serializer, option.name(), keyObj.toString(), valueObj.toString()); 203 } 204 } 205 } else { 206 dumpOptionToXml(serializer, option.name(), null, fieldVal.toString()); 207 } 208 } 209 } 210 211 /** 212 * Add a single option to the command XML dump. 213 * 214 * @param serializer a {@link KXmlSerializer} to create the XML dump 215 * @param name a {@link String} of the option's name 216 * @param key a {@link String} of the option's key, used as name if param name is null 217 * @param value a {@link String} of the option's value 218 */ dumpOptionToXml( KXmlSerializer serializer, String name, String key, String value)219 private static void dumpOptionToXml( 220 KXmlSerializer serializer, String name, String key, String value) throws IOException { 221 serializer.startTag(null, OPTION_NAME); 222 serializer.attribute(null, NAME_NAME, name); 223 if (key != null) { 224 serializer.attribute(null, KEY_NAME, key); 225 } 226 serializer.attribute(null, VALUE_NAME, value); 227 serializer.endTag(null, OPTION_NAME); 228 } 229 230 /** 231 * Helper to get the test config files from given directories. 232 * 233 * @param subPath where to look for configuration. Can be null. 234 * @param dirs a list of {@link File} of extra directories to search for test configs 235 */ getConfigNamesFromDirs(String subPath, List<File> dirs)236 public static Set<String> getConfigNamesFromDirs(String subPath, List<File> dirs) { 237 Set<File> res = getConfigNamesFileFromDirs(subPath, dirs); 238 if (res.isEmpty()) { 239 return new HashSet<>(); 240 } 241 Set<String> files = new HashSet<>(); 242 res.forEach(file -> files.add(file.getAbsolutePath())); 243 return files; 244 } 245 246 /** 247 * Helper to get the test config files from given directories. 248 * 249 * @param subPath The location where to look for configuration. Can be null. 250 * @param dirs A list of {@link File} of extra directories to search for test configs 251 * @return the set of {@link File} that were found. 252 */ getConfigNamesFileFromDirs(String subPath, List<File> dirs)253 public static Set<File> getConfigNamesFileFromDirs(String subPath, List<File> dirs) { 254 List<String> patterns = new ArrayList<>(); 255 patterns.add(".*\\.config$"); 256 patterns.add(".*\\.xml$"); 257 return getConfigNamesFileFromDirs(subPath, dirs, patterns); 258 } 259 260 /** 261 * Search a particular pattern of in the given directories. 262 * 263 * @param subPath The location where to look for configuration. Can be null. 264 * @param dirs A list of {@link File} of extra directories to search for test configs 265 * @param configNamePatterns the list of patterns for files to be found. 266 * @return the set of {@link File} that were found. 267 */ getConfigNamesFileFromDirs( String subPath, List<File> dirs, List<String> configNamePatterns)268 public static Set<File> getConfigNamesFileFromDirs( 269 String subPath, List<File> dirs, List<String> configNamePatterns) { 270 Set<File> configNames = new LinkedHashSet<>(); 271 for (File dir : dirs) { 272 if (subPath != null) { 273 dir = new File(dir, subPath); 274 } 275 if (!dir.isDirectory()) { 276 CLog.d("%s doesn't exist or is not a directory.", dir.getAbsolutePath()); 277 continue; 278 } 279 try { 280 for (String configNamePattern : configNamePatterns) { 281 configNames.addAll(FileUtil.findFilesObject(dir, configNamePattern)); 282 } 283 } catch (IOException e) { 284 CLog.w("Failed to get test config files from directory %s", dir.getAbsolutePath()); 285 } 286 } 287 return dedupFiles(configNames); 288 } 289 290 /** 291 * From a same tests dir we only expect a single instance of each names, so we dedup the files 292 * if that happens. 293 */ dedupFiles(Set<File> origSet)294 private static Set<File> dedupFiles(Set<File> origSet) { 295 Map<String, File> newMap = new LinkedHashMap<>(); 296 for (File f : origSet) { 297 try { 298 if (!FileUtil.readStringFromFile(f).contains("<configuration")) { 299 CLog.e("%s doesn't look like a test configuration.", f); 300 continue; 301 } 302 } catch (IOException e) { 303 CLog.e(e); 304 continue; 305 } 306 // Always keep the first found 307 if (!newMap.keySet().contains(f.getName())) { 308 newMap.put(f.getName(), f); 309 } 310 } 311 return new LinkedHashSet<>(newMap.values()); 312 } 313 } 314