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