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