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