1 /* 2 * Copyright (C) 2016 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 package com.android.compatibility.common.tradefed.util; 17 18 import com.android.tradefed.config.Option; 19 20 import java.lang.reflect.Field; 21 import java.util.ArrayList; 22 import java.util.HashSet; 23 import java.util.List; 24 import java.util.Set; 25 import java.util.regex.Matcher; 26 import java.util.regex.Pattern; 27 28 /** 29 * Helper class for manipulating fields with @option annotations. 30 */ 31 public final class OptionHelper { 32 OptionHelper()33 private OptionHelper() {} 34 35 /** 36 * Return the {@link List} of {@link Field} entries on the given object 37 * that have the {@link Option} annotation. 38 * 39 * @param object An object with @option-annotated fields. 40 */ getFields(Object object)41 private static List<Field> getFields(Object object) { 42 Field[] classFields = object.getClass().getDeclaredFields(); 43 List<Field> optionFields = new ArrayList<Field>(); 44 45 for (Field declaredField : classFields) { 46 // allow access to protected and private fields 47 declaredField.setAccessible(true); 48 49 // store type and values only in annotated fields 50 if (declaredField.isAnnotationPresent(Option.class)) { 51 optionFields.add(declaredField); 52 } 53 } 54 return optionFields; 55 } 56 57 /** 58 * Retrieve a {@link Set} of {@link Option} names present on the given 59 * object. 60 * 61 * @param object An object with @option-annotated fields. 62 */ getOptionNames(Object object)63 static Set<String> getOptionNames(Object object) { 64 Set<String> options = new HashSet<String>(); 65 List<Field> optionFields = getFields(object); 66 67 for (Field declaredField : optionFields) { 68 Option option = declaredField.getAnnotation(Option.class); 69 options.add(option.name()); 70 } 71 return options; 72 } 73 74 /** 75 * Retrieve a {@link Set} of {@link Option} short names present on the given 76 * object. 77 * 78 * @param object An object with @option-annotated fields. 79 */ getOptionShortNames(Object object)80 static Set<String> getOptionShortNames(Object object) { 81 Set<String> shortNames = new HashSet<String>(); 82 List<Field> optionFields = getFields(object); 83 84 for (Field declaredField : optionFields) { 85 Option option = declaredField.getAnnotation(Option.class); 86 if (option.shortName() != Option.NO_SHORT_NAME) { 87 shortNames.add(String.valueOf(option.shortName())); 88 } 89 } 90 return shortNames; 91 } 92 93 /** 94 * Retrieve a {@link List} of {@link String} entries of the valid 95 * command-line options for the given {@link Object} from the given 96 * input {@link String}. 97 */ getValidCliArgs(String commandString, Object object)98 public static List<String> getValidCliArgs(String commandString, Object object) { 99 Set<String> optionNames = OptionHelper.getOptionNames(object); 100 Set<String> optionShortNames = OptionHelper.getOptionShortNames(object); 101 List<String> validCliArgs = new ArrayList<String>(); 102 103 // get option/value substrings from the command-line string 104 // N.B. tradefed rewrites some expressions from option="value a b" to "option=value a b" 105 String quoteMatching = "(\"[^\"]+\")"; 106 String nonSpacedHypen = "((?<!\\s)-(?!\\s))"; 107 Pattern cliPattern = Pattern.compile( 108 // match -option=value or --option=value 109 "((-[-\\w]+([ =]" 110 // allow -option "...", -option x y z, and -option x:y:z and -option x:y:z1=z2 111 + "(" + quoteMatching + "|([\\w\\/\\s:=.]|"+ nonSpacedHypen + ")+))?" 112 + "))|" 113 // allow anything in direct quotes 114 + quoteMatching 115 ); 116 Matcher matcher = cliPattern.matcher(commandString); 117 118 while (matcher.find()) { 119 String optionInput = cleanNameValueDelimiter(matcher.group()); 120 // split between the option name and value 121 String[] keyNameTokens = optionInput.split("[ =]", 2); 122 // remove initial hyphens and any starting double quote from option args 123 String keyName = keyNameTokens[0].replaceFirst("^\"?--?", ""); 124 125 // Convert "option=value a b" back into option="value a b" 126 if (optionInput.charAt(0) == '"') { 127 optionInput = keyNameTokens[0].substring(1) + " \"" + keyNameTokens[1]; 128 } 129 130 // add substrings only when the options are recognized 131 if (optionShortNames.contains(keyName) || optionNames.contains(keyName)) { 132 // add values separated by spaces or in quotes separately to the return array 133 Pattern tokenPattern = Pattern.compile("(\".*\")|[^\\s]+"); 134 Matcher tokenMatcher = tokenPattern.matcher(optionInput); 135 while (tokenMatcher.find()) { 136 String token = tokenMatcher.group().replaceAll("\"", ""); 137 validCliArgs.add(token); 138 } 139 } 140 } 141 return validCliArgs; 142 } 143 144 /* 145 * The "=" sign is a valid character within an option value (such as in key-value map pairs). 146 * However, it may also act as delimiter between the option name and value. For simplicity, 147 * ensure the name-value delimiter is a space, so that we may treat the "=" as any other 148 * character in the getValidCliArgs() logic above. 149 */ cleanNameValueDelimiter(String optionNameValue)150 private static String cleanNameValueDelimiter(String optionNameValue) { 151 return optionNameValue.replaceFirst("[ =]", " "); 152 } 153 } 154