1 /* 2 * Copyright (C) 2024 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.util; 18 19 import com.android.tradefed.config.ConfigurationException; 20 import com.android.tradefed.config.Option; 21 import com.android.tradefed.config.OptionSetter; 22 import com.android.tradefed.config.OptionSetter.Handler; 23 import com.android.tradefed.config.OptionSetter.MapHandler; 24 import com.android.tradefed.log.LogUtil.CLog; 25 import com.android.tradefed.result.error.InfraErrorIdentifier; 26 27 import java.io.IOException; 28 import java.io.InputStream; 29 import java.lang.reflect.Field; 30 import java.util.Collection; 31 import java.util.Map; 32 import java.util.Properties; 33 import java.util.regex.Pattern; 34 35 /** 36 * A utility class that allows classes to load a variables value statically from a res file. 37 * 38 * <p>The resource file should be in a key=value format, where the key is associated with the 39 * variable that needs to be retrieved. A single resource file can contain multiple lines, where 40 * each line is associated with one variable. 41 * 42 * <p>To specify any primitive types, a single key=value pair should be used in a line. e.g.: 43 * 44 * <ol> 45 * <li>my-integer-key=5 46 * <li>my-string-key=myStringValue 47 * </ol> 48 * 49 * <p>To specify any collections, multiple values can be used, separated by a comma(,). e.g.: 50 * 51 * <ol> 52 * <li>my-string-list-key=stringOne,stringTwo,stringThree 53 * <li>my-int-list-key=1,2,3,4,5 54 * </ol> 55 * 56 * <p>To specify a map, multiple mapKey\=mapValue pair can be used, separated by a comma(,). e.g.: 57 * 58 * <ol> 59 * <li>my-map-key=mapKey1\=mapVal1,mapKey2\=mapVal2 60 * </ol> 61 */ 62 public class TfInternalOptionsFetcher { 63 private static String resourcePath = "/util/TfInternalOptions.properties"; 64 65 /** 66 * Fetches the values for all declared fields of the given {@link Class} from the specified 67 * resource file. If a resource file is not set, a default resource file will be used. 68 * 69 * @param classObj the class {@link Object} whose fields should be populated. 70 */ fetchOption(Class<?> classObj)71 public static void fetchOption(Class<?> classObj) { 72 try (InputStream stream = 73 TfInternalOptionsFetcher.class.getResourceAsStream(resourcePath)) { 74 // load the properties from the resource file 75 Properties properties = new Properties(); 76 properties.load(stream); 77 for (Field field : classObj.getDeclaredFields()) { 78 String propertyKey = field.getName(); 79 String optionVal = (String) properties.get(propertyKey); 80 // if property exists, update the field 81 if (optionVal != null) { 82 Handler handler = OptionSetter.getHandler(field.getGenericType()); 83 if (handler == null) { 84 throw new ConfigurationException( 85 String.format( 86 "Unable to get handler for option '%s'.", propertyKey), 87 InfraErrorIdentifier.OPTION_CONFIGURATION_ERROR); 88 } 89 boolean isOptionField = false; 90 if (field.getAnnotation(Option.class) != null) { 91 isOptionField = true; 92 } 93 if (Collection.class.isAssignableFrom(field.getType())) { 94 // if field is a collection, allow multiple values to be specified. 95 String[] optionValues = optionVal.split(","); 96 for (String optionValue : optionValues) { 97 // translate the value 98 Object translatedValue = handler.translate(optionValue); 99 OptionSetter.setFieldValue( 100 propertyKey, 101 classObj, 102 field, 103 null, 104 translatedValue, 105 isOptionField); 106 } 107 } else if (Map.class.isAssignableFrom(field.getType())) { 108 // if field is a map, allow multiple key=value pairs for the option value. 109 String[] optionValues = optionVal.split(","); 110 for (String optionValue : optionValues) { 111 String mapKey; 112 String mapVal; 113 // only match = to escape use "\=" 114 Pattern p = Pattern.compile("(?<!\\\\)="); 115 String[] parts = 116 p.split(optionValue, /* allow empty-string values */ -1); 117 // Note that we replace escaped = (\=) to =. 118 if (parts.length == 2) { 119 mapKey = parts[0].replaceAll("\\\\=", "="); 120 mapVal = parts[1].replaceAll("\\\\=", "="); 121 } else { 122 throw new ConfigurationException( 123 String.format( 124 "option '%s' has an invalid format for value %s:w", 125 propertyKey, optionValue), 126 InfraErrorIdentifier.OPTION_CONFIGURATION_ERROR); 127 } 128 // translate the key and value 129 Object translatedKey = ((MapHandler) handler).translateKey(mapKey); 130 Object translatedValue = handler.translate(mapVal); 131 OptionSetter.setFieldValue( 132 propertyKey, 133 classObj, 134 field, 135 translatedKey, 136 translatedValue, 137 isOptionField); 138 } 139 } else { 140 OptionSetter.setFieldValue( 141 propertyKey, 142 classObj, 143 field, 144 null, 145 handler.translate(optionVal), 146 isOptionField); 147 } 148 } 149 } 150 } catch (IOException | ConfigurationException e) { 151 CLog.w("Unable to fetch option values for class '%s'.", classObj.getName()); 152 CLog.e(e); 153 } 154 } 155 156 /** Set the path of the resource file where the value will be retrieved from. */ setResourcePath(String path)157 public static void setResourcePath(String path) { 158 resourcePath = path; 159 } 160 } 161