• 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 import java.util.Map.Entry;
24 
25 /**
26  * Base class for all options classes. Extend this class, adding public instance fields annotated
27  * with {@link Option}. Then you can create instances either programmatically:
28  *
29  * <pre>
30  *   X x = Options.getDefaults(X.class);
31  *   x.host = "localhost";
32  *   x.port = 80;
33  * </pre>
34  *
35  * or from an array of command-line arguments:
36  *
37  * <pre>
38  *   OptionsParser parser = OptionsParser.newOptionsParser(X.class);
39  *   parser.parse("--host", "localhost", "--port", "80");
40  *   X x = parser.getOptions(X.class);
41  * </pre>
42  *
43  * <p>Subclasses of {@code OptionsBase} <i>must</i> be constructed reflectively, i.e. using not
44  * {@code new MyOptions()}, but one of the above methods instead. (Direct construction creates an
45  * empty instance, not containing default values. This leads to surprising behavior and often {@code
46  * NullPointerExceptions}, etc.)
47  */
48 public abstract class OptionsBase {
49 
50   private static final Escaper ESCAPER = new CharEscaperBuilder()
51       .addEscape('\\', "\\\\").addEscape('"', "\\\"").toEscaper();
52 
53   /**
54    * Subclasses must provide a default (no argument) constructor.
55    */
OptionsBase()56   protected OptionsBase() {
57     // There used to be a sanity check here that checks the stack trace of this constructor
58     // invocation; unfortunately, that makes the options construction about 10x slower. So be
59     // careful with how you construct options classes.
60   }
61 
62   /**
63    * Returns a mapping from option names to values, for each option on this object, including
64    * inherited ones. The mapping is a copy, so subsequent mutations to it or to this object are
65    * independent. Entries are sorted alphabetically.
66    */
asMap()67   public final <O extends OptionsBase> Map<String, Object> asMap() {
68     // Generic O is needed to tell the type system that the toMap() call is safe.
69     // The casts are safe because "this" is an instance of "getClass()"
70     // which subclasses OptionsBase.
71     @SuppressWarnings("unchecked")
72     O castThis = (O) this;
73     @SuppressWarnings("unchecked")
74     Class<O> castClass = (Class<O>) getClass();
75 
76     Map<String, Object> map = new LinkedHashMap<>();
77     for (Map.Entry<Field, Object> entry : OptionsParser.toMap(castClass, castThis).entrySet()) {
78       String name = entry.getKey().getAnnotation(Option.class).name();
79       map.put(name, entry.getValue());
80     }
81     return map;
82   }
83 
84   @Override
toString()85   public final String toString() {
86     return getClass().getName() + asMap();
87   }
88 
89   /**
90    * Returns a string that uniquely identifies the options. This value is
91    * intended for analysis caching.
92    */
cacheKey()93   public final String cacheKey() {
94     StringBuilder result = new StringBuilder(getClass().getName()).append("{");
95 
96     for (Entry<String, Object> entry : asMap().entrySet()) {
97       result.append(entry.getKey()).append("=");
98 
99       Object value = entry.getValue();
100       // This special case is needed because List.toString() prints the same
101       // ("[]") for an empty list and for a list with a single empty string.
102       if (value instanceof List<?> && ((List<?>) value).isEmpty()) {
103         result.append("EMPTY");
104       } else if (value == null) {
105         result.append("NULL");
106       } else {
107         result
108             .append('"')
109             .append(ESCAPER.escape(value.toString()))
110             .append('"');
111       }
112       result.append(", ");
113     }
114 
115     return result.append("}").toString();
116   }
117 
118   @Override
equals(Object that)119   public final boolean equals(Object that) {
120     return that != null &&
121         this.getClass() == that.getClass() &&
122         this.asMap().equals(((OptionsBase) that).asMap());
123   }
124 
125   @Override
hashCode()126   public final int hashCode() {
127     return this.getClass().hashCode() + asMap().hashCode();
128   }
129 }
130