blob: ce66e66eef2bb656ca528942c244aa7954392174 [file] [log] [blame]
Damien Martin-Guillerezf88f4d82015-09-25 13:56:55 +00001// Copyright 2014 The Bazel Authors. All rights reserved.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +01002//
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
15package com.google.devtools.common.options;
16
17import com.google.common.escape.CharEscaperBuilder;
18import com.google.common.escape.Escaper;
brandjon4c2c4282017-04-19 19:22:56 +020019import java.lang.reflect.Field;
20import java.util.LinkedHashMap;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010021import java.util.List;
22import java.util.Map;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010023
24/**
brandjon4c2c4282017-04-19 19:22:56 +020025 * 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:
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010027 *
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 *
brandjon4c2c4282017-04-19 19:22:56 +020042 * <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.)
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010046 */
47public 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 */
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 /**
brandjon4c2c4282017-04-19 19:22:56 +020062 * 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.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010065 */
brandjon4c2c4282017-04-19 19:22:56 +020066 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.
brandjon46da1fc2017-04-26 21:23:09 +020068 // The casts are safe because "this" is an instance of "getClass()"
69 // which subclasses OptionsBase.
brandjon4c2c4282017-04-19 19:22:56 +020070 @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()) {
ccalvarine8aae032017-08-22 07:17:44 +020077 OptionDefinition optionDefinition = OptionDefinition.extractOptionDefinition(entry.getKey());
78 map.put(optionDefinition.getOptionName(), entry.getValue());
brandjon4c2c4282017-04-19 19:22:56 +020079 }
80 return map;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010081 }
82
83 @Override
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 */
92 public final String cacheKey() {
93 StringBuilder result = new StringBuilder(getClass().getName()).append("{");
juliexxiab470c0f2018-08-22 15:32:49 -070094 result.append(mapToCacheKey(asMap()));
95 return result.append("}").toString();
96 }
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010097
juliexxiab470c0f2018-08-22 15:32:49 -070098 public static String mapToCacheKey(Map<String, Object> optionsMap) {
99 StringBuilder result = new StringBuilder();
100 for (Map.Entry<String, Object> entry : optionsMap.entrySet()) {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100101 result.append(entry.getKey()).append("=");
102
103 Object value = entry.getValue();
104 // This special case is needed because List.toString() prints the same
105 // ("[]") for an empty list and for a list with a single empty string.
106 if (value instanceof List<?> && ((List<?>) value).isEmpty()) {
107 result.append("EMPTY");
108 } else if (value == null) {
109 result.append("NULL");
110 } else {
111 result
112 .append('"')
113 .append(ESCAPER.escape(value.toString()))
114 .append('"');
115 }
116 result.append(", ");
117 }
juliexxiab470c0f2018-08-22 15:32:49 -0700118 return result.toString();
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100119 }
120
121 @Override
122 public final boolean equals(Object that) {
juliexxiab470c0f2018-08-22 15:32:49 -0700123 return that instanceof OptionsBase && this.asMap().equals(((OptionsBase) that).asMap());
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100124 }
125
126 @Override
127 public final int hashCode() {
128 return this.getClass().hashCode() + asMap().hashCode();
129 }
130}