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 20 import java.util.List; 21 import java.util.Map; 22 import java.util.Map.Entry; 23 24 /** 25 * Base class for all options classes. Extend this class, adding public 26 * instance fields annotated with @Option. Then you can create instances 27 * 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 OptionsBase <b>must</b> be constructed reflectively, 44 * i.e. using not {@code new MyOptions}, but one of the two methods above 45 * instead. (Direct construction creates an empty instance, not containing 46 * default values. This leads to surprising behavior and often 47 * NullPointerExceptions, etc.) 48 */ 49 public abstract class OptionsBase { 50 51 private static final Escaper ESCAPER = new CharEscaperBuilder() 52 .addEscape('\\', "\\\\").addEscape('"', "\\\"").toEscaper(); 53 54 /** 55 * Subclasses must provide a default (no argument) constructor. 56 */ 57 protected OptionsBase() { 58 // There used to be a sanity check here that checks the stack trace of this constructor 59 // invocation; unfortunately, that makes the options construction about 10x slower. So be 60 // careful with how you construct options classes. 61 } 62 63 /** 64 * Returns this options object in the form of a (new) mapping from option 65 * names, including inherited ones, to option values. If the public fields 66 * are mutated, this will be reflected in subsequent calls to {@code asMap}. 67 * Mutation of this map by the caller does not affect this options object. 68 */ 69 public final Map<String, Object> asMap() { 70 return OptionsParserImpl.optionsAsMap(this); 71 } 72 73 @Override 74 public final String toString() { 75 return getClass().getName() + asMap(); 76 } 77 78 /** 79 * Returns a string that uniquely identifies the options. This value is 80 * intended for analysis caching. 81 */ 82 public final String cacheKey() { 83 StringBuilder result = new StringBuilder(getClass().getName()).append("{"); 84 85 for (Entry<String, Object> entry : asMap().entrySet()) { 86 result.append(entry.getKey()).append("="); 87 88 Object value = entry.getValue(); 89 // This special case is needed because List.toString() prints the same 90 // ("[]") for an empty list and for a list with a single empty string. 91 if (value instanceof List<?> && ((List<?>) value).isEmpty()) { 92 result.append("EMPTY"); 93 } else if (value == null) { 94 result.append("NULL"); 95 } else { 96 result 97 .append('"') 98 .append(ESCAPER.escape(value.toString())) 99 .append('"'); 100 } 101 result.append(", "); 102 } 103 104 return result.append("}").toString(); 105 } 106 107 @Override 108 public final boolean equals(Object that) { 109 return that != null && 110 this.getClass() == that.getClass() && 111 this.asMap().equals(((OptionsBase) that).asMap()); 112 } 113 114 @Override 115 public final int hashCode() { 116 return this.getClass().hashCode() + asMap().hashCode(); 117 } 118 } 119