content: /**
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License 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 org.apache.aurora.scheduler.config;
  
  import java.lang.reflect.Field;
  import java.lang.reflect.Modifier;
  import java.net.InetSocketAddress;
  import java.util.List;
  import java.util.Map;
  
  import javax.security.auth.kerberos.KerberosPrincipal;
  
  import com.beust.jcommander.IStringConverter;
  import com.beust.jcommander.IStringConverterFactory;
  import com.beust.jcommander.JCommander;
  import com.beust.jcommander.ParameterException;
  import com.google.common.annotations.VisibleForTesting;
  import com.google.common.base.Preconditions;
  import com.google.common.collect.ImmutableList;
  import com.google.common.collect.ImmutableMap;
  import com.google.common.collect.Lists;
  
  import org.apache.aurora.gen.DockerParameter;
  import org.apache.aurora.gen.Volume;
  import org.apache.aurora.scheduler.app.SchedulerMain;
  import org.apache.aurora.scheduler.app.VolumeConverter;
  import org.apache.aurora.scheduler.config.converters.ClassConverter;
  import org.apache.aurora.scheduler.config.converters.DataAmountConverter;
  import org.apache.aurora.scheduler.config.converters.DockerParameterConverter;
  import org.apache.aurora.scheduler.config.converters.InetSocketAddressConverter;
  import org.apache.aurora.scheduler.config.converters.TimeAmountConverter;
  import org.apache.aurora.scheduler.config.types.DataAmount;
  import org.apache.aurora.scheduler.config.types.TimeAmount;
  import org.apache.aurora.scheduler.http.api.security.KerberosPrincipalConverter;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  /**
   * Parses command line options and populates {@link CliOptions}.
   */
  public final class CommandLine {
  
    private static final Logger LOG = LoggerFactory.getLogger(CommandLine.class);
  
    // TODO(wfarner): This can go away if/when options are no longer accessed statically.
    private static CliOptions instance = null;
  
    private static List<Object> customOptions = Lists.newArrayList();
  
    private CommandLine() {
      // Utility class.
    }
  
    /**
     * Similar to {@link #initializeForTest()}, but resets the class to an un-parsed state.
     */
    @VisibleForTesting
    static void clearForTest() {
      instance = null;
      customOptions = Lists.newArrayList();
    }
  
    /**
     * Initializes static command line state - the static parsed instance, and custom options objects.
     */
    @VisibleForTesting
    public static void initializeForTest() {
      instance = new CliOptions();
      customOptions = Lists.newArrayList();
    }
  
    private static JCommander prepareParser(CliOptions options) {
      JCommander.Builder builder = JCommander.newBuilder()
          .programName(SchedulerMain.class.getName());
  
      builder.addConverterFactory(new IStringConverterFactory() {
        private Map<Class<?>, Class<? extends IStringConverter<?>>> classConverters =
            ImmutableMap.<Class<?>, Class<? extends IStringConverter<?>>>builder()
                .put(Class.class, ClassConverter.class)
                .put(DataAmount.class, DataAmountConverter.class)
                .put(DockerParameter.class, DockerParameterConverter.class)
                .put(InetSocketAddress.class, InetSocketAddressConverter.class)
                .put(KerberosPrincipal.class, KerberosPrincipalConverter.class)
                .put(TimeAmount.class, TimeAmountConverter.class)
                .put(Volume.class, VolumeConverter.class)
                .build();
  
        @SuppressWarnings("unchecked")
        @Override
        public <T> Class<? extends IStringConverter<T>> getConverter(Class<T> forType) {
          return (Class<IStringConverter<T>>) classConverters.get(forType);
        }
      });
  
      builder.addObject(getOptionsObjects(options));
      return builder.build();
    }
  
    /**
     * Applies arg values to the options object.
     *
     * @param args Command line arguments.
     */
    @VisibleForTesting
    public static CliOptions parseOptions(String... args) {
      JCommander parser = null;
      try {
        parser = prepareParser(new CliOptions(ImmutableList.copyOf(customOptions)));
  
        // We first perform a 'dummy' parsing round.  This induces classloading on any third-party
        // code, where they can statically invoke registerCustomOptions().
        parser.setAcceptUnknownOptions(true);
        parser.parseWithoutValidation(args);
  
        CliOptions options = new CliOptions(ImmutableList.copyOf(customOptions));
        parser = prepareParser(options);
        parser.parse(args);
  
        LOG.info("-----------------------------------------------------------------------");
        LOG.info("Parameters:");
        parser.getParameters().stream()
            .map(param ->
                param.getLongestName() + ": " + param.getParameterized().get(param.getObject()))
            .sorted()
            .forEach(LOG::info);
        LOG.info("-----------------------------------------------------------------------");
  
        instance = options;
        return options;
      } catch (ParameterException e) {
        if (parser != null) {
          parser.usage();
        }
        LOG.error(e.getMessage());
        System.exit(1);
        throw new RuntimeException(e);
      }
    }
  
    /**
     * Gets the static and globally-accessible CLI options.  This exists only to support legacy use
     * cases that cannot yet support injected arguments.  New callers should not be added.
     *
     * @return global options
     */
    public static CliOptions legacyGetStaticOptions() {
      if (instance == null) {
        throw new IllegalStateException("Attempted to fetch command line arguments before parsing.");
      }
      return instance;
    }
  
    /**
     * Registers a custom options container for inclusion during command line option parsing.  This
     * is useful to allow third-party modules to include custom command line options.
     *
     * @param options Custom options object.
     *                See {@link com.beust.jcommander.JCommander.Builder#addObject(Object)} for
     *                details.
     */
    public static void registerCustomOptions(Object options) {
      Preconditions.checkState(
          instance == null,
          "Attempted to register custom options after command line parsing.");
  
      customOptions.add(options);
    }
  
    @VisibleForTesting
    static List<Object> getOptionsObjects(CliOptions options) {
      ImmutableList.Builder<Object> objects = ImmutableList.builder();
  
      // Reflect on fields defined in CliOptions to DRY and avoid mistakes of forgetting to add an
      // option field here.
      for (Field field : CliOptions.class.getDeclaredFields()) {
        if (Modifier.isStatic(field.getModifiers())) {
          continue;
        }
  
        try {
          if (Iterable.class.isAssignableFrom(field.getType())) {
            Iterable<?> iterableValue = (Iterable<?>) field.get(options);
            objects.addAll(iterableValue);
          } else {
            objects.add(field.get(options));
          }
        } catch (IllegalAccessException e) {
          throw new RuntimeException(e);
        }
      }
  
      return objects.build();
    }
  }
  