1 /* 2 * Copyright 2015 The gRPC Authors 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 io.grpc.benchmarks.qps; 18 19 import static java.lang.Math.max; 20 import static java.lang.String.CASE_INSENSITIVE_ORDER; 21 22 import com.google.common.base.Strings; 23 import java.util.ArrayList; 24 import java.util.Collection; 25 import java.util.List; 26 import java.util.Locale; 27 import java.util.Map; 28 import java.util.Set; 29 import java.util.TreeMap; 30 import java.util.TreeSet; 31 32 /** 33 * Abstract base class for all {@link Configuration.Builder}s. 34 */ 35 public abstract class AbstractConfigurationBuilder<T extends Configuration> 36 implements Configuration.Builder<T> { 37 38 private static final Param HELP = new Param() { 39 @Override 40 public String getName() { 41 return "help"; 42 } 43 44 @Override 45 public String getType() { 46 return ""; 47 } 48 49 @Override 50 public String getDescription() { 51 return "Print this text."; 52 } 53 54 @Override 55 public boolean isRequired() { 56 return false; 57 } 58 59 @Override 60 public String getDefaultValue() { 61 return null; 62 } 63 64 @Override 65 public void setValue(Configuration config, String value) { 66 throw new UnsupportedOperationException(); 67 } 68 }; 69 70 /** 71 * A single application parameter supported by this builder. 72 */ 73 protected interface Param { 74 /** 75 * The name of the parameter as it would appear on the command-line. 76 */ getName()77 String getName(); 78 79 /** 80 * A string representation of the parameter type. If not applicable, just returns an empty 81 * string. 82 */ getType()83 String getType(); 84 85 /** 86 * A description of this parameter used when printing usage. 87 */ getDescription()88 String getDescription(); 89 90 /** 91 * The default value used when not set explicitly. Ignored if {@link #isRequired()} is {@code 92 * true}. 93 */ getDefaultValue()94 String getDefaultValue(); 95 96 /** 97 * Indicates whether or not this parameter is required and must therefore be set before the 98 * configuration can be successfully built. 99 */ isRequired()100 boolean isRequired(); 101 102 /** 103 * Sets this parameter on the given configuration instance. 104 */ setValue(Configuration config, String value)105 void setValue(Configuration config, String value); 106 } 107 108 @Override build(String[] args)109 public final T build(String[] args) { 110 T config = newConfiguration(); 111 Map<String, Param> paramMap = getParamMap(); 112 Set<String> appliedParams = new TreeSet<>(CASE_INSENSITIVE_ORDER); 113 114 for (String arg : args) { 115 if (!arg.startsWith("--")) { 116 throw new IllegalArgumentException("All arguments must start with '--': " + arg); 117 } 118 String[] pair = arg.substring(2).split("=", 2); 119 String key = pair[0]; 120 String value = ""; 121 if (pair.length == 2) { 122 value = pair[1]; 123 } 124 125 // If help was requested, just throw now to print out the usage. 126 if (HELP.getName().equalsIgnoreCase(key)) { 127 throw new IllegalArgumentException("Help requested"); 128 } 129 130 Param param = paramMap.get(key); 131 if (param == null) { 132 throw new IllegalArgumentException("Unsupported argument: " + key); 133 } 134 param.setValue(config, value); 135 appliedParams.add(key); 136 } 137 138 // Ensure that all required options have been provided. 139 for (Param param : getParams()) { 140 if (param.isRequired() && !appliedParams.contains(param.getName())) { 141 throw new IllegalArgumentException("Missing required option '--" 142 + param.getName() + "'."); 143 } 144 } 145 146 return build0(config); 147 } 148 149 @Override 150 @SuppressWarnings("InlineMeInliner") // String.repeat() requires Java 11 printUsage()151 public final void printUsage() { 152 System.out.println("Usage: [ARGS...]"); 153 int column1Width = 0; 154 List<Param> params = new ArrayList<>(); 155 params.add(HELP); 156 params.addAll(getParams()); 157 158 for (Param param : params) { 159 column1Width = max(commandLineFlag(param).length(), column1Width); 160 } 161 int column1Start = 2; 162 int column2Start = column1Start + column1Width + 2; 163 for (Param param : params) { 164 StringBuilder sb = new StringBuilder(); 165 sb.append(Strings.repeat(" ", column1Start)); 166 sb.append(commandLineFlag(param)); 167 sb.append(Strings.repeat(" ", column2Start - sb.length())); 168 String message = param.getDescription(); 169 sb.append(wordWrap(message, column2Start, 80)); 170 if (param.isRequired()) { 171 sb.append(Strings.repeat(" ", column2Start)); 172 sb.append("[Required]\n"); 173 } else if (param.getDefaultValue() != null && !param.getDefaultValue().isEmpty()) { 174 sb.append(Strings.repeat(" ", column2Start)); 175 sb.append("[Default=" + param.getDefaultValue() + "]\n"); 176 } 177 System.out.println(sb); 178 } 179 System.out.println(); 180 } 181 182 /** 183 * Creates a new configuration instance which will be used as the target for command-line 184 * arguments. 185 */ newConfiguration()186 protected abstract T newConfiguration(); 187 188 /** 189 * Returns the valid parameters supported by the configuration. 190 */ getParams()191 protected abstract Collection<Param> getParams(); 192 193 /** 194 * Called by {@link #build(String[])} after verifying that all required options have been set. 195 * Performs any final validation and modifications to the configuration. If successful, returns 196 * the fully built configuration. 197 */ build0(T config)198 protected abstract T build0(T config); 199 getParamMap()200 private Map<String, Param> getParamMap() { 201 Map<String, Param> map = new TreeMap<>(CASE_INSENSITIVE_ORDER); 202 for (Param param : getParams()) { 203 map.put(param.getName(), param); 204 } 205 return map; 206 } 207 commandLineFlag(Param param)208 private static String commandLineFlag(Param param) { 209 String name = param.getName().toLowerCase(Locale.ROOT); 210 String type = (!param.getType().isEmpty() ? '=' + param.getType() : ""); 211 return "--" + name + type; 212 } 213 214 @SuppressWarnings("InlineMeInliner") // String.repeat() requires Java 11 wordWrap(String text, int startPos, int maxPos)215 private static String wordWrap(String text, int startPos, int maxPos) { 216 StringBuilder builder = new StringBuilder(); 217 int pos = startPos; 218 String[] parts = text.split("\\n", -1); 219 boolean isBulleted = parts.length > 1; 220 for (String part : parts) { 221 int lineStart = startPos; 222 while (!part.isEmpty()) { 223 if (pos < lineStart) { 224 builder.append(Strings.repeat(" ", lineStart - pos)); 225 pos = lineStart; 226 } 227 int maxLength = maxPos - pos; 228 int length = part.length(); 229 if (length > maxLength) { 230 length = part.lastIndexOf(' ', maxPos - pos) + 1; 231 if (length == 0) { 232 length = part.length(); 233 } 234 } 235 builder.append(part.substring(0, length)); 236 part = part.substring(length); 237 238 // Wrap to the next line. 239 builder.append("\n"); 240 pos = 0; 241 lineStart = isBulleted ? startPos + 2 : startPos; 242 } 243 } 244 return builder.toString(); 245 } 246 } 247