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