• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2014 The Bazel Authors. All rights reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //    http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 package com.google.devtools.common.options;
16 
17 import com.google.common.escape.CharEscaperBuilder;
18 import com.google.common.escape.Escaper;
19 import java.lang.reflect.Field;
20 import java.util.LinkedHashMap;
21 import java.util.List;
22 import java.util.Map;
23 
24 /**
25  * Base class for all options classes. Extend this class, adding public instance fields annotated
26  * with {@link Option}. Then you can create instances either programmatically:
27  *
28  * <pre>
29  *   X x = Options.getDefaults(X.class);
30  *   x.host = "localhost";
31  *   x.port = 80;
32  * </pre>
33  *
34  * or from an array of command-line arguments:
35  *
36  * <pre>
37  *   OptionsParser parser = OptionsParser.newOptionsParser(X.class);
38  *   parser.parse("--host", "localhost", "--port", "80");
39  *   X x = parser.getOptions(X.class);
40  * </pre>
41  *
42  * <p>Subclasses of {@code OptionsBase} <i>must</i> be constructed reflectively, i.e. using not
43  * {@code new MyOptions()}, but one of the above methods instead. (Direct construction creates an
44  * empty instance, not containing default values. This leads to surprising behavior and often {@code
45  * NullPointerExceptions}, etc.)
46  */
47 public abstract class OptionsBase {
48 
49   private static final Escaper ESCAPER = new CharEscaperBuilder()
50       .addEscape('\\', "\\\\").addEscape('"', "\\\"").toEscaper();
51 
52   /**
53    * Subclasses must provide a default (no argument) constructor.
54    */
OptionsBase()55   protected OptionsBase() {
56     // There used to be a sanity check here that checks the stack trace of this constructor
57     // invocation; unfortunately, that makes the options construction about 10x slower. So be
58     // careful with how you construct options classes.
59   }
60 
61   /**
62    * Returns a mapping from option names to values, for each option on this object, including
63    * inherited ones. The mapping is a copy, so subsequent mutations to it or to this object are
64    * independent. Entries are sorted alphabetically.
65    */
asMap()66   public final <O extends OptionsBase> Map<String, Object> asMap() {
67     // Generic O is needed to tell the type system that the toMap() call is safe.
68     // The casts are safe because "this" is an instance of "getClass()"
69     // which subclasses OptionsBase.
70     @SuppressWarnings("unchecked")
71     O castThis = (O) this;
72     @SuppressWarnings("unchecked")
73     Class<O> castClass = (Class<O>) getClass();
74 
75     Map<String, Object> map = new LinkedHashMap<>();
76     for (Map.Entry<Field, Object> entry : OptionsParser.toMap(castClass, castThis).entrySet()) {
77       OptionDefinition optionDefinition = OptionDefinition.extractOptionDefinition(entry.getKey());
78       map.put(optionDefinition.getOptionName(), entry.getValue());
79     }
80     return map;
81   }
82 
83   @Override
toString()84   public final String toString() {
85     return getClass().getName() + asMap();
86   }
87 
88   /**
89    * Returns a string that uniquely identifies the options. This value is
90    * intended for analysis caching.
91    */
cacheKey()92   public final String cacheKey() {
93     StringBuilder result = new StringBuilder(getClass().getName()).append("{");
94 
95     for (Map.Entry<String, Object> entry : asMap().entrySet()) {
96       result.append(entry.getKey()).append("=");
97 
98       Object value = entry.getValue();
99       // This special case is needed because List.toString() prints the same
100       // ("[]") for an empty list and for a list with a single empty string.
101       if (value instanceof List<?> && ((List<?>) value).isEmpty()) {
102         result.append("EMPTY");
103       } else if (value == null) {
104         result.append("NULL");
105       } else {
106         result
107             .append('"')
108             .append(ESCAPER.escape(value.toString()))
109             .append('"');
110       }
111       result.append(", ");
112     }
113 
114     return result.append("}").toString();
115   }
116 
117   @Override
equals(Object that)118   public final boolean equals(Object that) {
119     return that != null &&
120         this.getClass() == that.getClass() &&
121         this.asMap().equals(((OptionsBase) that).asMap());
122   }
123 
124   @Override
hashCode()125   public final int hashCode() {
126     return this.getClass().hashCode() + asMap().hashCode();
127   }
128 }
129