• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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