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