All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.checkerframework.checker.i18nformatter.util.I18nFormatUtil Maven / Gradle / Ivy

Go to download

The Checker Framework enhances Java's type system to make it more powerful and useful. This lets software developers detect and prevent errors in their Java programs. The Checker Framework includes compiler plug-ins ("checkers") that find bugs or verify their absence. It also permits you to write your own compiler plug-ins.

There is a newer version: 3.43.0
Show newest version
package org.checkerframework.checker.i18nformatter.util;

import java.text.ChoiceFormat;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.IllegalFormatException;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.checkerframework.checker.i18nformatter.qual.I18nChecksFormat;
import org.checkerframework.checker.i18nformatter.qual.I18nConversionCategory;
import org.checkerframework.checker.i18nformatter.qual.I18nValidFormat;
import org.checkerframework.checker.interning.qual.InternedDistinct;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
import org.checkerframework.framework.qual.AnnotatedFor;

/**
 * This class provides a collection of utilities to ease working with i18n format strings.
 *
 * @checker_framework.manual #i18n-formatter-checker Internationalization Format String Checker
 */
@AnnotatedFor("nullness")
public class I18nFormatUtil {

  /**
   * Throws an exception if the format is not syntactically valid.
   *
   * @param format the format string to parse
   */
  @SuppressWarnings("nullness:argument") // It's not documented, but passing null as the
  // argument array is supported.
  public static void tryFormatSatisfiability(String format) throws IllegalFormatException {
    MessageFormat.format(format, (Object[]) null);
  }

  /**
   * Returns a {@link I18nConversionCategory} for every conversion found in the format string.
   *
   * @param format the format string to parse
   * @throws IllegalFormatException if the format is not syntactically valid
   */
  public static I18nConversionCategory[] formatParameterCategories(String format)
      throws IllegalFormatException {
    tryFormatSatisfiability(format);
    I18nConversion[] cs = MessageFormatParser.parse(format);

    int maxIndex = -1;
    Map conv = new HashMap<>(cs.length);

    for (I18nConversion c : cs) {
      int index = c.index;
      Integer indexKey = index;
      conv.put(
          indexKey,
          I18nConversionCategory.intersect(
              c.category,
              conv.containsKey(indexKey) ? conv.get(indexKey) : I18nConversionCategory.UNUSED));
      maxIndex = Math.max(maxIndex, index);
    }

    I18nConversionCategory[] res = new I18nConversionCategory[maxIndex + 1];
    for (int i = 0; i <= maxIndex; i++) {
      Integer indexKey = i;
      res[i] = conv.containsKey(indexKey) ? conv.get(indexKey) : I18nConversionCategory.UNUSED;
    }
    return res;
  }

  /**
   * Returns true if the format string is satisfiable, and if the format's parameters match the
   * passed {@link I18nConversionCategory}s. Otherwise an error is thrown.
   *
   * @param format a format string
   * @param cc a list of expected categories for the string's format specifiers
   * @return true if the format string's specifiers are the given categories, in order
   */
  // TODO introduce more such functions, see RegexUtil for examples
  @I18nChecksFormat
  public static boolean hasFormat(String format, I18nConversionCategory... cc) {
    I18nConversionCategory[] fcc = formatParameterCategories(format);
    if (fcc.length != cc.length) {
      return false;
    }

    for (int i = 0; i < cc.length; i++) {
      if (!I18nConversionCategory.isSubsetOf(cc[i], fcc[i])) {
        return false;
      }
    }
    return true;
  }

  @I18nValidFormat
  public static boolean isFormat(String format) {
    try {
      formatParameterCategories(format);
    } catch (Exception e) {
      return false;
    }
    return true;
  }

  /** An I18n cenversion directive. */
  private static class I18nConversion {
    /** The index into the string. */
    public final int index;

    /** The conversion category. */
    public final I18nConversionCategory category;

    /**
     * Creates a new I18nConversion.
     *
     * @param index the index into the string
     * @param category the conversion category
     */
    public I18nConversion(int index, I18nConversionCategory category) {
      this.index = index;
      this.category = category;
    }

    @Override
    public String toString() {
      return category.toString() + "(index: " + index + ")";
    }
  }

  private static class MessageFormatParser {

    public static int maxOffset;

    /** The locale to use for formatting numbers and dates. Is set in {@link #parse}. */
    private static @MonotonicNonNull Locale locale;

    /** An array of formatters, which are used to format the arguments. Is set in {@link #parse}. */
    private static @MonotonicNonNull List categories;

    /**
     * The argument numbers corresponding to each formatter. (The formatters are stored in the order
     * they occur in the pattern, not in the order in which the arguments are specified.) Is set in
     * {@link #parse}.
     */
    private static @MonotonicNonNull List argumentIndices;

    // I think this means the number of format specifiers in the format string.
    /** The number of subformats. */
    private static int numFormat;

    // Indices for segments
    private static final int SEG_RAW = 0;
    private static final int SEG_INDEX = 1;
    private static final int SEG_TYPE = 2;
    private static final int SEG_MODIFIER = 3; // modifier or subformat

    // Indices for type keywords
    private static final int TYPE_NULL = 0;
    private static final int TYPE_NUMBER = 1;
    private static final int TYPE_DATE = 2;
    private static final int TYPE_TIME = 3;
    private static final int TYPE_CHOICE = 4;

    private static final String[] TYPE_KEYWORDS = {"", "number", "date", "time", "choice"};

    // Indices for number modifiers
    private static final int MODIFIER_DEFAULT = 0; // common in number and date-time
    private static final int MODIFIER_CURRENCY = 1;
    private static final int MODIFIER_PERCENT = 2;
    private static final int MODIFIER_INTEGER = 3;

    private static final String[] NUMBER_MODIFIER_KEYWORDS = {"", "currency", "percent", "integer"};

    private static final String[] DATE_TIME_MODIFIER_KEYWORDS = {
      "", "short", "medium", "long", "full"
    };

    @EnsuresNonNull({"categories", "argumentIndices", "locale"})
    public static I18nConversion[] parse(String pattern) {
      MessageFormatParser.categories = new ArrayList<>();
      MessageFormatParser.argumentIndices = new ArrayList<>();
      MessageFormatParser.locale = Locale.getDefault(Locale.Category.FORMAT);
      applyPattern(pattern);

      I18nConversion[] ret = new I18nConversion[MessageFormatParser.numFormat];
      for (int i = 0; i < MessageFormatParser.numFormat; i++) {
        ret[i] = new I18nConversion(argumentIndices.get(i), categories.get(i));
      }
      return ret;
    }

    @SuppressWarnings("nullness:dereference.of.nullable") // complex rules for segments[i]
    @RequiresNonNull({"argumentIndices", "categories", "locale"})
    private static void applyPattern(String pattern) {
      @Nullable StringBuilder[] segments = new StringBuilder[4];
      // Allocate only segments[SEG_RAW] here. The rest are
      // allocated on demand.
      segments[SEG_RAW] = new StringBuilder();

      int part = SEG_RAW;
      MessageFormatParser.numFormat = 0;
      boolean inQuote = false;
      int braceStack = 0;
      maxOffset = -1;
      for (int i = 0; i < pattern.length(); ++i) {
        char ch = pattern.charAt(i);
        if (part == SEG_RAW) {
          if (ch == '\'') {
            if (i + 1 < pattern.length() && pattern.charAt(i + 1) == '\'') {
              segments[part].append(ch); // handle doubles
              ++i;
            } else {
              inQuote = !inQuote;
            }
          } else if (ch == '{' && !inQuote) {
            part = SEG_INDEX;
            if (segments[SEG_INDEX] == null) {
              segments[SEG_INDEX] = new StringBuilder();
            }
          } 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 < SEG_MODIFIER) {
                  if (segments[++part] == null) {
                    segments[part] = new StringBuilder();
                  }
                } else {
                  segments[part].append(ch);
                }
                break;
              case '{':
                ++braceStack;
                segments[part].append(ch);
                break;
              case '}':
                if (braceStack == 0) {
                  part = SEG_RAW;
                  makeFormat(numFormat, segments);
                  numFormat++;
                  // throw away other segments
                  segments[SEG_INDEX] = null;
                  segments[SEG_TYPE] = null;
                  segments[SEG_MODIFIER] = null;
                } else {
                  --braceStack;
                  segments[part].append(ch);
                }
                break;
              case ' ':
                // Skip any leading space chars for SEG_TYPE.
                if (part != SEG_TYPE || segments[SEG_TYPE].length() > 0) {
                  segments[part].append(ch);
                }
                break;
              case '\'':
                inQuote = true;
                segments[part].append(ch);
                break;
              default:
                segments[part].append(ch);
                break;
            }
          }
        }
      }
      if (braceStack == 0 && part != 0) {
        maxOffset = -1;
        throw new IllegalArgumentException("Unmatched braces in the pattern");
      }
    }

    /** Side-effects {@code categories} field, adding to it an I18nConversionCategory. */
    @RequiresNonNull({"argumentIndices", "categories", "locale"})
    private static void makeFormat(int offsetNumber, @Nullable StringBuilder[] textSegments) {
      String[] segments = new String[textSegments.length];
      for (int i = 0; i < textSegments.length; i++) {
        StringBuilder oneseg = textSegments[i];
        segments[i] = (oneseg != null) ? oneseg.toString() : "";
      }

      // get the argument number
      int argumentNumber;
      try {
        argumentNumber = Integer.parseInt(segments[SEG_INDEX]); // always
        // unlocalized!
      } catch (NumberFormatException e) {
        throw new IllegalArgumentException(
            "can't parse argument number: " + segments[SEG_INDEX], e);
      }
      if (argumentNumber < 0) {
        throw new IllegalArgumentException("negative argument number: " + argumentNumber);
      }

      int oldMaxOffset = maxOffset;
      maxOffset = offsetNumber;
      argumentIndices.add(argumentNumber);

      // now get the format
      final I18nConversionCategory category;
      if (segments[SEG_TYPE].length() != 0) {
        int type = findKeyword(segments[SEG_TYPE], TYPE_KEYWORDS);
        switch (type) {
          case TYPE_NULL:
            category = I18nConversionCategory.GENERAL;
            break;
          case TYPE_NUMBER:
            switch (findKeyword(segments[SEG_MODIFIER], NUMBER_MODIFIER_KEYWORDS)) {
              case MODIFIER_DEFAULT:
              case MODIFIER_CURRENCY:
              case MODIFIER_PERCENT:
              case MODIFIER_INTEGER:
                break;
              default: // DecimalFormat pattern
                try {
                  new DecimalFormat(
                      segments[SEG_MODIFIER], DecimalFormatSymbols.getInstance(locale));
                } catch (IllegalArgumentException e) {
                  maxOffset = oldMaxOffset;
                  // invalid decimal subformat pattern
                  throw e;
                }
                break;
            }
            category = I18nConversionCategory.NUMBER;
            break;
          case TYPE_DATE:
          case TYPE_TIME:
            int mod = findKeyword(segments[SEG_MODIFIER], DATE_TIME_MODIFIER_KEYWORDS);
            if (mod >= 0 && mod < DATE_TIME_MODIFIER_KEYWORDS.length) {
              // nothing to do
            } else {
              // SimpleDateFormat pattern
              try {
                new SimpleDateFormat(segments[SEG_MODIFIER], locale);
              } catch (IllegalArgumentException e) {
                maxOffset = oldMaxOffset;
                // invalid date subformat pattern
                throw e;
              }
            }
            category = I18nConversionCategory.DATE;
            break;
          case TYPE_CHOICE:
            if (segments[SEG_MODIFIER].length() == 0) {
              throw new IllegalArgumentException(
                  "Choice Pattern requires Subformat Pattern: " + segments[SEG_MODIFIER]);
            }
            try {
              // ChoiceFormat pattern
              new ChoiceFormat(segments[SEG_MODIFIER]);
            } catch (Exception e) {
              maxOffset = oldMaxOffset;
              // invalid choice subformat pattern
              throw new IllegalArgumentException(
                  "Choice Pattern incorrect: " + segments[SEG_MODIFIER], e);
            }
            category = I18nConversionCategory.NUMBER;
            break;
          default:
            maxOffset = oldMaxOffset;
            throw new IllegalArgumentException("unknown format type: " + segments[SEG_TYPE]);
        }
      } else {
        category = I18nConversionCategory.GENERAL;
      }
      categories.add(category);
    }

    /**
     * Return the index of s in list. If not found, return the index of
     * s.trim().toLowerCase(Locale.ROOT) in list. If still not found, return -1.
     */
    private static int findKeyword(String s, String[] list) {
      for (int i = 0; i < list.length; ++i) {
        if (s.equals(list[i])) {
          return i;
        }
      }

      // Try trimmed lowercase.
      @SuppressWarnings("interning:assignment") // test if value changed
      @InternedDistinct String ls = s.trim().toLowerCase(Locale.ROOT);
      if (ls != s) { // Don't loop if the string trim().toLowerCase returned the same object.
        for (int i = 0; i < list.length; ++i) {
          if (ls.equals(list[i])) {
            return i;
          }
        }
      }
      return -1;
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy