content: /*
   * Licensed to the Apache Software Foundation (ASF) under one or more
   * contributor license agreements.  See the NOTICE file distributed with
   * this work for additional information regarding copyright ownership.
   * The ASF licenses this file to you 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.calcite.util;
  
  import com.google.common.collect.ImmutableList;
  
  import org.checkerframework.checker.nullness.qual.Nullable;
  
  import java.text.MessageFormat;
  import java.util.ArrayList;
  import java.util.List;
  import java.util.Locale;
  import java.util.Map;
  
  /**
   * String template.
   *
   * <p>It is extended from {@link java.text.MessageFormat} to allow parameters
   * to be substituted by name as well as by position.
   *
   * <p>The following example, using MessageFormat, yields "Happy 64th birthday,
   * Ringo!":
   *
   * <blockquote>MessageFormat f =
   * new MessageFormat("Happy {0,number}th birthday, {1}!");<br>
   * Object[] args = {64, "Ringo"};<br>
   * System.out.println(f.format(args);</blockquote>
   *
   * <p>Here is the same example using a Template and named parameters:
   *
   * <blockquote>Template f =
   * new Template("Happy {age,number}th birthday, {name}!");<br>
   * Map&lt;Object, Object&gt; args = new HashMap&lt;Object, Object&gt;();<br>
   * args.put("age", 64);<br>
   * args.put("name", "Ringo");<br>
   * System.out.println(f.format(args);</blockquote>
   *
   * <p>Using a Template you can also use positional parameters:
   *
   * <blockquote>Template f =
   * new Template("Happy {age,number}th birthday, {name}!");<br>
   * Object[] args = {64, "Ringo"};<br>
   * System.out.println(f.format(args);</blockquote>
   *
   * <p>Or a hybrid; here, one argument is specified by name, another by position:
   *
   * <blockquote>Template f =
   * new Template("Happy {age,number}th birthday, {name}!");<br>
   * Map&lt;Object, Object&gt; args = new HashMap&lt;Object, Object&gt;();<br>
   * args.put(0, 64);<br>
   * args.put("name", "Ringo");<br>
   * System.out.println(f.format(args);</blockquote>
   */
  public class Template extends MessageFormat {
    private final List<String> parameterNames;
  
    /**
     * Creates a Template for the default locale and the
     * specified pattern.
     *
     * @param pattern the pattern for this message format
     * @throws IllegalArgumentException if the pattern is invalid
     */
    public static Template of(String pattern) {
      return of(pattern, Locale.getDefault());
    }
  
    /**
     * Creates a Template for the specified locale and
     * pattern.
     *
     * @param pattern the pattern for this message format
     * @param locale  the locale for this message format
     * @throws IllegalArgumentException if the pattern is invalid
     */
    public static Template of(String pattern, Locale locale) {
      final List<String> parameterNames = new ArrayList<>();
      final String processedPattern = process(pattern, parameterNames);
      return new Template(processedPattern, parameterNames, locale);
    }
  
    private Template(
        String pattern, List<String> parameterNames, Locale locale) {
      super(pattern, locale);
      this.parameterNames = ImmutableList.copyOf(parameterNames);
    }
  
    /**
     * Parses the pattern, populates the parameter names, and returns the
     * pattern with parameter names converted to parameter ordinals.
     *
     * <p>To ensure that the same parsing rules apply, this code is copied from
     * {@link java.text.MessageFormat#applyPattern(String)} but with different
     * actions when a parameter is recognized.
     *
     * @param pattern        Pattern
     * @param parameterNames Names of parameters (output)
     * @return Pattern with named parameters substituted with ordinals
     */
    private static String process(String pattern, List<String> parameterNames) {
      StringBuilder[] segments = new StringBuilder[4];
      for (int i = 0; i < segments.length; ++i) {
        segments[i] = new StringBuilder();
      }
      int part = 0;
      boolean inQuote = false;
      int braceStack = 0;
      for (int i = 0; i < pattern.length(); ++i) {
        char ch = pattern.charAt(i);
        if (part == 0) {
          if (ch == '\'') {
            segments[part].append(ch); // jhyde: don't lose orig quote
            if (i + 1 < pattern.length()
                && pattern.charAt(i + 1) == '\'') {
              segments[part].append(ch);  // handle doubles
              ++i;
            } else {
              inQuote = !inQuote;
            }
          } else if (ch == '{' && !inQuote) {
            part = 1;
          } else {
            segments[part].append(ch);
          }
        } else if (inQuote) {              // just copy quotes in parts
          segments[part].append(ch);
          if (ch == '\'') {
            inQuote = false;
          }
        } else {
          switch (ch) {
          case ',':
            if (part < 3) {
              part += 1;
            } else {
              segments[part].append(ch);
            }
            break;
          case '{':
            ++braceStack;
            segments[part].append(ch);
            break;
          case '}':
            if (braceStack == 0) {
              part = 0;
              makeFormat(segments, parameterNames);
            } else {
              --braceStack;
              segments[part].append(ch);
            }
            break;
          case '\'':
            inQuote = true;
            // fall through, so we keep quotes in other parts
          default:
            segments[part].append(ch);
            break;
          }
        }
      }
      if (braceStack == 0 && part != 0) {
        throw new IllegalArgumentException(
            "Unmatched braces in the pattern.");
      }
      return segments[0].toString();
    }
  
    /**
     * Called when a complete parameter has been seen.
     *
     * @param segments       Comma-separated segments of the parameter definition
     * @param parameterNames List of parameter names seen so far
     */
    private static void makeFormat(
        StringBuilder[] segments,
        List<String> parameterNames) {
      final String parameterName = segments[1].toString();
      final int parameterOrdinal = parameterNames.size();
      parameterNames.add(parameterName);
      segments[0].append("{");
      segments[0].append(parameterOrdinal);
      final String two = segments[2].toString();
      if (two.length() > 0) {
        segments[0].append(",").append(two);
      }
      final String three = segments[3].toString();
      if (three.length() > 0) {
        segments[0].append(",").append(three);
      }
      segments[0].append("}");
      segments[1].setLength(0);   // throw away other segments
      segments[2].setLength(0);
      segments[3].setLength(0);
    }
  
    /**
     * Formats a set of arguments to produce a string.
     *
     * <p>Arguments may appear in the map using named keys (of type String), or
     * positional keys (0-based ordinals represented either as type String or
     * Integer).
     *
     * @param argMap A map containing the arguments as (key, value) pairs
     * @return Formatted string.
     * @throws IllegalArgumentException if the Format cannot format the given
     *                                  object
     */
    public String format(Map<Object, Object> argMap) {
      @Nullable Object[] args = new Object[parameterNames.size()];
      for (int i = 0; i < parameterNames.size(); i++) {
        args[i] = getArg(argMap, i);
      }
      return format(args);
    }
  
    /**
     * Returns the value of the {@code ordinal}th argument.
     *
     * @param argMap  Map of argument values
     * @param ordinal Ordinal of argument
     * @return Value of argument
     */
    private @Nullable Object getArg(Map<Object, Object> argMap, int ordinal) {
      // First get by name.
      String parameterName = parameterNames.get(ordinal);
      Object arg = argMap.get(parameterName);
      if (arg != null) {
        return arg;
      }
      // Next by integer ordinal.
      arg = argMap.get(ordinal);
      if (arg != null) {
        return arg;
      }
      // Next by string ordinal.
      return argMap.get(ordinal + "");
    }
  
    /**
     * Creates a Template with the given pattern and uses it
     * to format the given arguments. This is equivalent to
     * <blockquote>
     * <code>{@link #of(String) Template}(pattern).{@link #format}(args)</code>
     * </blockquote>
     *
     * @throws IllegalArgumentException if the pattern is invalid,
     *                                  or if an argument in the
     *                                  <code>arguments</code> array is not of the
     *                                  type expected by the format element(s)
     *                                  that use it.
     */
    public static String formatByName(
        String pattern,
        Map<Object, Object> argMap) {
      return Template.of(pattern).format(argMap);
    }
  
    /**
     * Returns the names of the parameters, in the order that they appeared in
     * the template string.
     *
     * @return List of parameter names
     */
    public List<String> getParameterNames() {
      return parameterNames;
    }
  }
  