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

com.squarespace.cldr.codegen.CodeGenerator Maven / Gradle / Ivy

There is a newer version: 0.0.26
Show newest version
package com.squarespace.cldr.codegen;

import static com.squarespace.cldr.codegen.Types.CLDR;
import static com.squarespace.cldr.codegen.Types.CLDR_BASE;
import static com.squarespace.cldr.codegen.Types.CLDR_LOCALE_IF;
import static com.squarespace.cldr.codegen.Types.LIST_CLDR_LOCALE_IF;
import static com.squarespace.cldr.codegen.Types.META_LOCALE;
import static com.squarespace.cldr.codegen.Types.PACKAGE_CLDR;
import static com.squarespace.cldr.codegen.Types.PLURAL_RULES;
import static com.squarespace.cldr.codegen.Types.STRING;
import static javax.lang.model.element.Modifier.ABSTRACT;
import static javax.lang.model.element.Modifier.FINAL;
import static javax.lang.model.element.Modifier.PRIVATE;
import static javax.lang.model.element.Modifier.PUBLIC;
import static javax.lang.model.element.Modifier.STATIC;

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import com.google.common.base.Charsets;
import com.google.common.base.Splitter;
import com.google.common.io.CharSink;
import com.google.common.io.Files;
import com.squarespace.cldr.codegen.reader.DataReader;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;


/**
 * Generates the CLDR classes that encapsulate the core data.
 *
 * TODO: embed version/build information into generated classes
 */
public class CodeGenerator {

  private static final String[] EMPTY = new String[] { };

  public static void main(String[] args) throws IOException {
    Path root = Paths.get("/Users/phensley/dev/squarespace-cldr");
    generate(root.resolve("runtime/src/generated/java"));
  }

  /**
   * Loads all CLDR data and invokes the code generators for each data type. Output
   * is a series of Java classes under the outputDir.
   */
  public static void generate(Path outputDir) throws IOException {
    DataReader reader = DataReader.get();
    CalendarCodeGenerator datetimeGenerator = new CalendarCodeGenerator();
    Map dateClasses = datetimeGenerator.generate(outputDir, reader);

    PluralCodeGenerator pluralGenerator = new PluralCodeGenerator();
    pluralGenerator.generate(outputDir, reader);

    NumberCodeGenerator numberGenerator = new NumberCodeGenerator();
    Map numberClasses = numberGenerator.generate(outputDir, reader);
    
    LanguageCodeGenerator languageGenerator = new LanguageCodeGenerator();
    languageGenerator.generate(outputDir, reader);
    
    MethodSpec registerCalendars = indexFormatters("registerCalendars", "registerCalendarFormatter", dateClasses);
    MethodSpec registerNumbers = indexFormatters("registerNumbers", "registerNumberFormatter", numberClasses);
    
    MethodSpec constructor = MethodSpec.constructorBuilder()
        .addModifiers(PRIVATE)
        .build();

    FieldSpec instance = FieldSpec.builder(CLDR, "instance", PRIVATE, STATIC, FINAL)
        .build();

    MethodSpec getter = MethodSpec.methodBuilder("get")
        .addModifiers(PUBLIC, STATIC)
        .returns(CLDR)
        .addStatement("return instance")
        .build();

    TypeSpec.Builder type = TypeSpec.classBuilder("CLDR")
        .addModifiers(PUBLIC)
        .superclass(CLDR_BASE)
        .addMethod(constructor)
        .addMethod(getter)
        .addMethod(registerCalendars)
        .addMethod(registerNumbers);

    Set availableLocales = reader.availableLocales()
        .stream()
        .map(s -> LocaleID.parse(s))
        .collect(Collectors.toSet());
    
    createLocales(type, reader.defaultContent(), availableLocales);
    createLanguageAliases(type, reader.languageAliases());
    createTerritoryAliases(type, reader.territoryAliases());
    createLikelySubtags(type, reader.likelySubtags());
    createCurrencies(type, numberGenerator.getCurrencies(reader));
    
    addPluralRules(type);

    // Initialize all static maps.
    type.addStaticBlock(CodeBlock.builder()
      .addStatement("registerCalendars()")
      .addStatement("registerNumbers()")
      .addStatement("registerDefaultContent()")
      .addStatement("registerLanguageAliases()")
      .addStatement("registerTerritoryAliases()")
      .addStatement("registerLikelySubtags()")
      .addStatement("instance = new CLDR()").build());
    
    type.addField(instance);

    saveClass(outputDir, PACKAGE_CLDR, "CLDR", type.build());
  }

  /**
   * Saves a Java class file to a path for the given package, rooted in rootDir.
   */
  public static void saveClass(Path rootDir, String packageName, String className, TypeSpec type)
      throws IOException {

    List packagePath = Splitter.on('.').splitToList(packageName);
    Path classPath = Paths.get(rootDir.toString(), packagePath.toArray(EMPTY));
    Path outputFile = classPath.resolve(className + ".java");

    JavaFile javaFile = JavaFile.builder(packageName, type)
        .addFileComment("\n\nAUTO-GENERATED CLASS - DO NOT EDIT\n\n")
        // TODO: build timestamp and version info
        .build();

    System.out.println("saving " + outputFile);
    Files.createParentDirs(outputFile.toFile());
    CharSink sink = Files.asCharSink(outputFile.toFile(), Charsets.UTF_8);
    sink.write(javaFile.toString());
  }

  /**
   * Add static instance of plural rules and accessor method.
   */
  private static void addPluralRules(TypeSpec.Builder type) {
    FieldSpec field = FieldSpec.builder(PLURAL_RULES, "pluralRules", PRIVATE, STATIC, FINAL)
        .initializer("new $T()", PLURAL_RULES)
        .build();
    
    MethodSpec method = MethodSpec.methodBuilder("getPluralRules")
        .addModifiers(PUBLIC)
        .returns(PLURAL_RULES)
        .addStatement("return pluralRules")
        .build();

    type.addField(field);
    type.addMethod(method);
  }
  
  /**
   * Generates a static code block that populates the formatter map.
   */
  private static MethodSpec indexFormatters(
      String methodName, String registerMethodName, Map dateClasses) {
    
    MethodSpec.Builder method = MethodSpec.methodBuilder(methodName)
        .addModifiers(PRIVATE, STATIC);
    
    for (Map.Entry entry : dateClasses.entrySet()) {
      LocaleID localeId = entry.getKey();
      ClassName className = entry.getValue();
      method.addStatement("$T.$L(Locale.$L, $L.class)", CLDR_BASE, 
          registerMethodName, localeId.safe, className);
    }
    return method.build();
  }
  
  /**
   * Generate the mapping for default content locales.
   * See: http://cldr.unicode.org/translation/default-content
   */
  private static void createLocales(TypeSpec.Builder type, List defaultContent, Set available) {
    TypeSpec.Builder localeInterface = TypeSpec.interfaceBuilder("Locale")
        .addModifiers(PUBLIC, STATIC)
        .addMethod(MethodSpec.methodBuilder("language")
            .addModifiers(PUBLIC, ABSTRACT)
            .returns(String.class).build())
        .addMethod(MethodSpec.methodBuilder("script")
            .addModifiers(PUBLIC, ABSTRACT)
            .returns(String.class).build())
        .addMethod(MethodSpec.methodBuilder("territory")
            .addModifiers(PUBLIC, ABSTRACT)
            .returns(String.class).build())
        .addMethod(MethodSpec.methodBuilder("variant")
            .addModifiers(PUBLIC, ABSTRACT)
            .returns(String.class).build())
        .addMethod(MethodSpec.methodBuilder("compact")
            .addModifiers(PUBLIC, ABSTRACT)
            .returns(String.class).build())
        .addMethod(MethodSpec.methodBuilder("expanded")
            .addModifiers(PUBLIC, ABSTRACT)
            .returns(String.class).build());
    
    List localeFields = new ArrayList<>();
    List allLocales = new ArrayList<>();
    List bundles = new ArrayList<>();
    
    for (LocaleID locale : available) {
      localeFields.add(locale);
      allLocales.add(locale.safe);
      bundles.add(locale.safe);
    }
    
    MethodSpec.Builder method = MethodSpec.methodBuilder("registerDefaultContent")
        .addModifiers(PRIVATE, STATIC);
    
    for (LocaleID locale : defaultContent) {
      LocaleID dest = new LocaleID(locale.language, "", "", "");
      if (available.contains(dest)) {
        localeFields.add(locale);
        allLocales.add(locale.safe);
        method.addStatement("DEFAULT_CONTENT.put(Locale.$L, Locale.$L)",
            locale.safe,
            dest.safe);
      }
    }
    type.addMethod(method.build());

    Collections.sort(localeFields);
    for (LocaleID locale : localeFields) {
      addLocaleField(localeInterface, locale.safe, locale);
    }

    StringBuilder buf = new StringBuilder("$T.unmodifiableList($T.asList(\n");
    for (int i = 0; i < allLocales.size(); i++) {
      if (i > 0) {
        buf.append(",\n");
      }
      buf.append("  Locale.$L");
    }
    buf.append("))");

    Collections.sort(allLocales);
    
    List arguments = new ArrayList<>();
    arguments.add(Collections.class);
    arguments.add(Arrays.class);
    arguments.addAll(allLocales);
    
    FieldSpec.Builder field = FieldSpec.builder(LIST_CLDR_LOCALE_IF, "AVAILABLE_LOCALES", PRIVATE, STATIC, FINAL);
    field.initializer(buf.toString(), arguments.toArray());
    type.addField(field.build());
    
    buf = new StringBuilder("$T.unmodifiableList($T.asList(\n");
    for (int i = 0; i < bundles.size(); i++) {
      if (i > 0) {
        buf.append(",\n");
      }
      buf.append("  Locale.$L");
    }
    buf.append("))");
    
    Collections.sort(bundles);
    arguments = new ArrayList<>();
    arguments.add(Collections.class);
    arguments.add(Arrays.class);
    arguments.addAll(bundles);
    
    field = FieldSpec.builder(LIST_CLDR_LOCALE_IF, "AVAILABLE_BUNDLES", PRIVATE, STATIC, FINAL);
    field.initializer(buf.toString(), arguments.toArray());
    type.addField(field.build());
    
    method = MethodSpec.methodBuilder("availableLocales")
        .addModifiers(PUBLIC, FINAL)
        .returns(LIST_CLDR_LOCALE_IF)
        .addStatement("return AVAILABLE_LOCALES");
    
    type.addMethod(method.build());
    
    method = MethodSpec.methodBuilder("availableBundles")
        .addModifiers(PUBLIC, FINAL)
        .returns(LIST_CLDR_LOCALE_IF)
        .addStatement("return AVAILABLE_BUNDLES");
    
    type.addMethod(method.build());

    type.addType(localeInterface.build());
  }

  /**
   * Create a public locale field.
   */
  private static void addLocaleField(TypeSpec.Builder type, String name, LocaleID locale) {
    FieldSpec.Builder field = FieldSpec.builder(CLDR_LOCALE_IF, name, PUBLIC, STATIC, FINAL)
        .initializer("new $T($S, $S, $S, $S)",
            META_LOCALE,
            strOrNull(locale.language), 
            strOrNull(locale.script),
            strOrNull(locale.territory),
            strOrNull(locale.variant));
    type.addField(field.build());
  }
  
  /**
   * Create language alias mapping.
   */
  private static void createLanguageAliases(TypeSpec.Builder type, Map languageAliases) {
    MethodSpec.Builder method = MethodSpec.methodBuilder("registerLanguageAliases")
        .addModifiers(PRIVATE, STATIC);
    
    for (Map.Entry entry : languageAliases.entrySet()) {
      method.addStatement("addLanguageAlias($S, $S)", 
          entry.getKey(), entry.getValue());
    }
    
    type.addMethod(method.build());
  }
  
  private static void createTerritoryAliases(TypeSpec.Builder type, Map territoryAliases) {
    MethodSpec.Builder method = MethodSpec.methodBuilder("registerTerritoryAliases")
        .addModifiers(PRIVATE, STATIC);
    
    for (Map.Entry entry : territoryAliases.entrySet()) {
      method.addStatement("addTerritoryAlias($S, $S)", 
          entry.getKey(), entry.getValue());
    }
    
    type.addMethod(method.build());
  }

  /**
   * Create likely subtags mapping.
   */
  private static void createLikelySubtags(TypeSpec.Builder type, Map likelySubtags) {
    MethodSpec.Builder method = MethodSpec.methodBuilder("registerLikelySubtags")
        .addModifiers(PRIVATE, STATIC);
    
    for (Map.Entry entry : likelySubtags.entrySet()) {
      method.addStatement("LIKELY_SUBTAGS_MAP.put($T.parse($S), $T.parse($S))",
          META_LOCALE, entry.getKey(), META_LOCALE, entry.getValue());
    }
    
    type.addMethod(method.build());
  }
  
  private static String strOrNull(String value) {
    return value.equals("") ? null : value;
  }
  
  /**
   * Create top-level container to hold currency constants.
   */
  private static void createCurrencies(TypeSpec.Builder type, List currencies) {
    TypeSpec.Builder currencyType = TypeSpec.enumBuilder("Currency")
        .addModifiers(PUBLIC, STATIC);

    List codes = new ArrayList<>();
    codes.addAll(currencies);
    Collections.sort(codes);
    
    StringBuilder buf = new StringBuilder("$T.unmodifiableList($T.asList(\n");
    for (int i = 0; i < codes.size(); i++) {
      if (i > 0) {
        buf.append(",\n");
      }
      currencyType.addEnumConstant(codes.get(i));
      buf.append("  Currency.$L");
    }
    buf.append("))");
    
    // Add a safe string mapping that returns null instead of throwing.
    MethodSpec.Builder method = MethodSpec.methodBuilder("fromString")
        .addModifiers(PUBLIC, STATIC)
        .addParameter(STRING, "code")
        .returns(Types.CLDR_CURRENCY_ENUM);

    method.beginControlFlow("if (code != null)");
    method.beginControlFlow("switch (code)");
    for (int i = 0; i < codes.size(); i++) {
      method.addStatement("case $S: return $L", codes.get(i), codes.get(i));
    }
    method.addStatement("default: break");
    method.endControlFlow();
    method.endControlFlow();
    method.addStatement("return null");
    
    currencyType.addMethod(method.build());
    
    type.addType(currencyType.build());

    // Initialize field containing all currencies
    List arguments = new ArrayList<>();
    arguments.add(Collections.class);
    arguments.add(Arrays.class);
    arguments.addAll(codes);
  }
  
}