// ================================================================================================= // Copyright 2012 Twitter, Inc. // ------------------------------------------------------------------------------------------------- // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this work except in compliance with the License. // You may obtain a copy of the License in the LICENSE file, or at: // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ================================================================================================= package com.twitter.common.args; import java.io.File; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.util.Arrays; import java.util.List; import java.util.regex.Pattern; import javax.annotation.Nullable; import com.google.common.base.Charsets; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Predicates; import com.google.common.base.Strings; import com.google.common.io.Files; import com.google.common.reflect.TypeToken; import com.twitter.common.args.apt.Configuration; import com.twitter.common.base.Function; import static com.google.common.base.Preconditions.checkArgument; /** * Description of a command line option/flag such as -foo=bar. */ public final class OptionInfo<T> extends ArgumentInfo<T> { static final String ARG_NAME_RE = "[\\w\\-\\.]+"; static final String ARG_FILE_HELP_TEMPLATE = "%s This argument supports @argfile format. See details below."; private static final Pattern ARG_NAME_PATTERN = Pattern.compile(ARG_NAME_RE); private static final String NEGATE_BOOLEAN = "no_"; private static final String ARG_FILE_INDICATOR = "@"; /** * Factory method to create a OptionInfo from a field. * * @param field The field must contain a {@link Arg}. * @return an OptionInfo describing the field. */ static OptionInfo<?> createFromField(Field field) { return createFromField(field, null); } /** * Factory method to create a OptionInfo from a field. * * @param field The field must contain a {@link Arg}. * @param instance The object containing the non-static Arg instance or else null if the Arg * field is static. * @return an OptionInfo describing the field. */ static OptionInfo<?> createFromField(final Field field, @Nullable Object instance) { CmdLine cmdLine = field.getAnnotation(CmdLine.class); if (cmdLine == null) { throw new Configuration.ConfigurationException( "No @CmdLine Arg annotation for field " + field); } String name = cmdLine.name(); Preconditions.checkNotNull(name); checkArgument(!HELP_ARGS.contains(name), String.format("Argument name '%s' is reserved for builtin argument help", name)); checkArgument(ARG_NAME_PATTERN.matcher(name).matches(), String.format("Argument name '%s' does not match required pattern %s", name, ARG_NAME_RE)); Function<String, String> canonicalizer = new Function<String, String>() { @Override public String apply(String name) { return field.getDeclaringClass().getCanonicalName() + "." + name; } }; @SuppressWarnings({"unchecked", "rawtypes"}) // we have no way to know the type here OptionInfo<?> optionInfo = new OptionInfo( canonicalizer, name, getCmdLineHelp(cmdLine), cmdLine.argFile(), ArgumentInfo.getArgForField(field, Optional.fromNullable(instance)), TypeUtil.getTypeParamTypeToken(field), Arrays.asList(field.getAnnotations()), cmdLine.parser()); return optionInfo; } private static String getCmdLineHelp(CmdLine cmdLine) { String help = cmdLine.help(); if (cmdLine.argFile()) { help = String.format(ARG_FILE_HELP_TEMPLATE, help, cmdLine.name(), cmdLine.name()); } return help; } private final Function<String, String> canonicalizer; private OptionInfo( Function<String, String> canonicalizer, String name, String help, boolean argFile, Arg<T> arg, TypeToken<T> type, List<Annotation> verifierAnnotations, @Nullable Class<? extends Parser<T>> parser) { super(canonicalizer.apply(name), name, help, argFile, arg, type, verifierAnnotations, parser); this.canonicalizer = canonicalizer; } /** * Parses the value and store result in the {@link Arg} contained in this {@code OptionInfo}. */ void load(ParserOracle parserOracle, String optionName, String value) { Parser<? extends T> parser = getParser(parserOracle); String finalValue = value; // If "-arg=@file" is allowed and specified, then we read the value from the file // and use it as the raw value to be parsed for the argument. if (argFile() && !Strings.isNullOrEmpty(value) && value.startsWith(ARG_FILE_INDICATOR)) { finalValue = getArgFileContent(optionName, value.substring(ARG_FILE_INDICATOR.length())); } Object result = parser.parse(parserOracle, getType().getType(), finalValue); // [A] // If the arg type is boolean, check if the command line uses the negated boolean form. if (isBoolean()) { if (Predicates.in(Arrays.asList(getNegatedName(), getCanonicalNegatedName())) .apply(optionName)) { result = !(Boolean) result; // [B] } } // We know result is T at line [A] but throw this type information away to allow negation if T // is Boolean at line [B] @SuppressWarnings("unchecked") T parsed = (T) result; setValue(parsed); } boolean isBoolean() { return getType().getRawType() == Boolean.class; } /** * Similar to the simple name, but with boolean arguments appends "no_", as in: * {@code -no_fire=false} */ String getNegatedName() { return NEGATE_BOOLEAN + getName(); } /** * Similar to the canonical name, but with boolean arguments appends "no_", as in: * {@code -com.twitter.common.MyApp.no_fire=false} */ String getCanonicalNegatedName() { return canonicalizer.apply(getNegatedName()); } private String getArgFileContent(String optionName, String argFilePath) throws IllegalArgumentException { if (argFilePath.isEmpty()) { throw new IllegalArgumentException( String.format("Invalid null/empty value for argument '%s'.", optionName)); } try { return Files.toString(new File(argFilePath), Charsets.UTF_8); } catch (IOException e) { throw new IllegalArgumentException( String.format("Unable to read argument '%s' value from file '%s'.", optionName, argFilePath), e); } } }