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