/* * Copyright 2010 Google Inc. * * 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 com.google.gwt.tools.cldr; import com.google.gwt.codegen.server.StringGenerator; import com.google.gwt.core.ext.UnableToCompleteException; import com.google.gwt.i18n.client.DateTimeFormatInfo; import com.google.gwt.i18n.client.impl.cldr.DateTimeFormatInfoImpl; import com.google.gwt.i18n.rebind.DateTimePatternGenerator; import com.google.gwt.i18n.rebind.MessageFormatParser; import com.google.gwt.i18n.rebind.MessageFormatParser.ArgumentChunk; import com.google.gwt.i18n.rebind.MessageFormatParser.DefaultTemplateChunkVisitor; import com.google.gwt.i18n.rebind.MessageFormatParser.StringChunk; import com.google.gwt.i18n.rebind.MessageFormatParser.TemplateChunk; import com.google.gwt.i18n.shared.GwtLocale; import org.unicode.cldr.util.CLDRFile.Factory; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.text.ParseException; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Map.Entry; import java.util.SortedMap; import java.util.TreeMap; /** * Loads data needed to produce DateTimeFormatInfo implementations. */ public class DateTimeFormatInfoProcessor extends Processor { private static final String[] DAYS = new String[] { "sun", "mon", "tue", "wed", "thu", "fri", "sat"}; /** * Map of skeleton format patterns and the method name suffix that uses them. */ private static final Map<String, String> FORMATS; /** * Index of the formats, ordered by the method name. */ private static final SortedMap<String, String> FORMAT_BY_METHOD; static { FORMATS = new HashMap<String, String>(); FORMATS.put("d", "Day"); FORMATS.put("hmm", "Hour12Minute"); FORMATS.put("hmmss", "Hour12MinuteSecond"); FORMATS.put("Hmm", "Hour24Minute"); FORMATS.put("Hmmss", "Hour24MinuteSecond"); FORMATS.put("mss", "MinuteSecond"); FORMATS.put("MMM", "MonthAbbrev"); FORMATS.put("MMMd", "MonthAbbrevDay"); FORMATS.put("MMMM", "MonthFull"); FORMATS.put("MMMMd", "MonthFullDay"); FORMATS.put("MMMMEEEEd", "MonthFullWeekdayDay"); FORMATS.put("Md", "MonthNumDay"); FORMATS.put("y", "Year"); FORMATS.put("yMMM", "YearMonthAbbrev"); FORMATS.put("yMMMd", "YearMonthAbbrevDay"); FORMATS.put("yMMMM", "YearMonthFull"); FORMATS.put("yMMMMd", "YearMonthFullDay"); FORMATS.put("yM", "YearMonthNum"); FORMATS.put("yMd", "YearMonthNumDay"); FORMATS.put("yMMMEEEd", "YearMonthWeekdayDay"); FORMATS.put("yQQQQ", "YearQuarterFull"); FORMATS.put("yQ", "YearQuarterShort"); FORMAT_BY_METHOD = new TreeMap<String, String>(); for (Map.Entry<String, String> entry : FORMATS.entrySet()) { FORMAT_BY_METHOD.put(entry.getValue(), entry.getKey()); } } /** * Convert the unlocalized name of a day ("sun".."sat") into a day number of * the week, ie 0-6. * * @param day abbreviated, unlocalized name of the day ("sun".."sat") * @return the day number, 0-6 * @throws IllegalArgumentException if the day name is not found */ private static int getDayNumber(String day) { for (int i = 0; i < DAYS.length; ++i) { if (DAYS[i].equals(day)) { return i; } } throw new IllegalArgumentException(); } private final RegionLanguageData regionLanguageData; public DateTimeFormatInfoProcessor(File outputDir, Factory cldrFactory, LocaleData localeData) { super(outputDir, cldrFactory, localeData); regionLanguageData = new RegionLanguageData(cldrFactory); } @Override protected void cleanupData() { System.out.println("Removing duplicates from date/time formats"); localeData.copyLocaleData("en", "default", "era-wide", "era-abbrev", "quarter-wide", "quarter-abbrev", "day-wide", "day-sa-wide", "day-narrow", "day-sa-narrow", "day-abbrev", "day-sa-abbrev", "month-wide", "month-sa-wide", "month-narrow", "month-sa-narrow", "month-abbrev", "month-sa-abbrev"); removeUnusedFormats(); localeData.removeDuplicates("predef"); localeData.removeDuplicates("weekdata"); localeData.removeDuplicates("date"); localeData.removeDuplicates("time"); localeData.removeDuplicates("dateTime"); localeData.removeCompleteDuplicates("dayPeriod-abbrev"); computePeriodRedirects("day"); computePeriodRedirects("month"); computePeriodRedirects("day"); removePeriodDuplicates("day"); removePeriodDuplicates("month"); removePeriodDuplicates("quarter"); removePeriodDuplicates("era"); } /** * Generate an override for a method which takes String arguments, which * simply redirect to another method based on a default value. * * @param pw * @param category * @param locale * @param method * @param args */ protected void generateArgMethod(PrintWriter pw, String category, GwtLocale locale, String method, String... args) { String value = localeData.getEntry(category, locale, "default"); if (value != null && value.length() > 0) { pw.println(); if (getOverrides()) { pw.println(" @Override"); } pw.print(" public String " + method + "("); String prefix = ""; for (String arg : args) { pw.print(prefix + "String " + arg); prefix = ", "; } pw.println(") {"); pw.print(" return " + method + Character.toTitleCase(value.charAt(0)) + value.substring(1) + "("); prefix = ""; for (String arg : args) { pw.print(prefix + arg); prefix = ", "; } pw.println(");"); pw.println(" }"); } } /** * Generate an override for a method which takes String arguments. * * @param pw * @param category * @param locale * @param key * @param method * @param args */ protected void generateArgMethodRedirect(PrintWriter pw, String category, GwtLocale locale, String key, String method, final String... args) { String value = localeData.getEntry(category, locale, key); if (value != null) { pw.println(); if (getOverrides()) { pw.println(" @Override"); } pw.print(" public String " + method + "("); String prefix = ""; for (String arg : args) { pw.print(prefix + "String " + arg); prefix = ", "; } pw.println(") {"); final StringBuilder buf = new StringBuilder(); final StringGenerator gen = StringGenerator.create(buf, false); try { List<TemplateChunk> chunks = MessageFormatParser.parse(value); for (TemplateChunk chunk : chunks) { chunk.accept(new DefaultTemplateChunkVisitor() { @Override public void visit(ArgumentChunk argChunk) throws UnableToCompleteException { gen.appendStringValuedExpression(args[argChunk.getArgumentNumber()]); } @Override public void visit(StringChunk stringChunk) throws UnableToCompleteException { gen.appendStringLiteral(stringChunk.getString()); } }); } } catch (ParseException e) { throw new RuntimeException("Unable to parse pattern '" + value + "' for locale " + locale + " key " + category + "/" + key, e); } catch (UnableToCompleteException e) { throw new RuntimeException("Unable to parse pattern '" + value + "' for locale " + locale + " key " + category + "/" + key, e); } gen.completeString(); pw.println(" return " + buf.toString() + ";"); pw.println(" }"); } } /** * Generate a method which returns a day number as an integer. * * @param pw * @param locale * @param key * @param method */ protected void generateDayNumber(PrintWriter pw, GwtLocale locale, String key, String method) { String day = localeData.getEntry("weekdata", locale, key); if (day != null) { int value = getDayNumber(day); pw.println(); if (getOverrides()) { pw.println(" @Override"); } pw.println(" public int " + method + "() {"); pw.println(" return " + value + ";"); pw.println(" }"); } } /** * Generate a method which returns a format string for a given predefined * skeleton pattern. * * @param locale * @param pw * @param skeleton * @param methodSuffix */ protected void generateFormat(GwtLocale locale, PrintWriter pw, String skeleton, String methodSuffix) { String pattern = localeData.getEntry("predef", locale, skeleton); generateStringValue(pw, "format" + methodSuffix, pattern); } /** * Generate a series of methods which returns names in wide, narrow, and * abbreviated lengths plus their standalone versions. * * @param pw * @param group * @param locale * @param methodPrefix * @param keys */ protected void generateFullStringList(PrintWriter pw, String group, GwtLocale locale, String methodPrefix, String... keys) { generateStringListPair(pw, group, locale, methodPrefix, "Full", "wide", keys); generateStringListPair(pw, group, locale, methodPrefix, "Narrow", "narrow", keys); generateStringListPair(pw, group, locale, methodPrefix, "Short", "abbrev", keys); } /** * Generate an override of a standalone names list that simply redirects to * the non-standalone version. * * @param pw * @param methodPrefix */ protected void generateStandaloneRedirect(PrintWriter pw, String methodPrefix) { pw.println(); if (getOverrides()) { pw.println(" @Override"); } pw.println(" public String[] " + methodPrefix + "Standalone" + "() {"); pw.println(" return " + methodPrefix + "();"); pw.println(" }"); } /** * Generate a method which returns a list of strings. * * @param pw * @param category * @param fallbackCategory * @param locale * @param method * @param keys * @return true if the method was skipped as identical to its ancestor */ protected boolean generateStringList(PrintWriter pw, String category, String fallbackCategory, GwtLocale locale, String method, String... keys) { Map<String, String> map = localeData.getEntries(category, locale); Map<String, String> fallback = fallbackCategory == null ? Collections.<String, String> emptyMap() : localeData.getEntries( fallbackCategory, locale); if (map == null || map.isEmpty() && fallback != null && !fallback.isEmpty()) { return true; } if (map != null && !map.isEmpty()) { if (fallbackCategory != null) { // see if the entry is the same as the fallback boolean different = false; for (String key : keys) { String value = map.get(key); if (value != null && !value.equals(fallback.get(key))) { different = true; break; } } if (!different) { return true; } } pw.println(); if (getOverrides()) { pw.println(" @Override"); } pw.println(" public String[] " + method + "() {"); pw.print(" return new String[] {"); boolean first = true; for (String key : keys) { String value = map.get(key); if (value == null) { value = fallback.get(key); } if (value == null) { System.err.println("Missing \"" + key + "\" in " + locale + "/" + category); value = ""; } if (first) { first = false; } else { pw.print(","); } pw.print("\n \"" + value.replace("\"", "\\\"") + "\""); } pw.println("\n };"); pw.println(" }"); } return false; } protected void generateStringListPair(PrintWriter pw, String group, GwtLocale locale, String methodPrefix, String width, String categorySuffix, String... keys) { generateStringList(pw, group + "-" + categorySuffix, null, locale, methodPrefix + width, keys); String redirect = localeData.getEntry(group + "-sa-" + categorySuffix + "-redirect", locale, "redirect"); if ("yes".equals(redirect)) { generateStandaloneRedirect(pw, methodPrefix + width); } else { generateStringList(pw, group + "-sa-" + categorySuffix, group + "-" + categorySuffix, locale, methodPrefix + width + "Standalone", keys); } } @Override protected void loadData() throws IOException { System.out.println("Loading data for date/time formats"); localeData.addEntries("predef", cldrFactory, "//ldml/dates/calendars/calendar[@type=\"gregorian\"]/dateTimeFormats/" + "availableFormats", "dateFormatItem", "id"); localeData.addNameEntries("month", cldrFactory); localeData.addNameEntries("day", cldrFactory); localeData.addNameEntries("quarter", cldrFactory); // only add the entries we will use to avoid overriding a parent for // differences that don't matter. localeData.addEntries("dayPeriod-abbrev", cldrFactory, "//ldml/dates/calendars/calendar[@type=\"gregorian\"]/dayPeriods/" + "dayPeriodContext[@type=\"format\"]/" + "dayPeriodWidth[@type=\"abbreviated\"]/dayPeriod[@type=\"am\"]", "dayPeriod", "type"); localeData.addEntries("dayPeriod-abbrev", cldrFactory, "//ldml/dates/calendars/calendar[@type=\"gregorian\"]/dayPeriods/" + "dayPeriodContext[@type=\"format\"]/" + "dayPeriodWidth[@type=\"abbreviated\"]/dayPeriod[@type=\"pm\"]", "dayPeriod", "type"); localeData.addEntries("era-abbrev", cldrFactory, "//ldml/dates/calendars/calendar[@type=\"gregorian\"]/eras/eraAbbr", "era", "type"); localeData.addEntries("era-wide", cldrFactory, "//ldml/dates/calendars/calendar[@type=\"gregorian\"]/eras/eraNames", "era", "type"); localeData.addDateTimeFormatEntries("date", cldrFactory); localeData.addDateTimeFormatEntries("time", cldrFactory); localeData.addDateTimeFormatEntries("dateTime", cldrFactory); loadWeekData(); loadFormatPatterns(); } @Override protected void writeOutputFiles() throws IOException { // TODO(jat): make uz_UZ inherit from uz_Cyrl rather than uz, for example System.out.println("Writing output for date/time formats"); for (GwtLocale locale : localeData.getNonEmptyLocales()) { String myClass; String path = "client/"; String pathSuffix = ""; if (locale.isDefault()) { myClass = "DefaultDateTimeFormatInfo"; } else { myClass = "DateTimeFormatInfoImpl" + localeSuffix(locale); pathSuffix = "impl/cldr/"; } GwtLocale parent = localeData.inheritsFrom(locale); PrintWriter pw = createOutputFile(path + pathSuffix + myClass + ".java"); printHeader(pw); pw.print("package com.google.gwt.i18n.client"); if (!locale.isDefault()) { pw.print(".impl.cldr"); setOverrides(true); } else { setOverrides(false); } pw.println(";"); pw.println(); pw.println("// DO NOT EDIT - GENERATED FROM CLDR AND ICU DATA"); pw.println(); if (locale.isDefault()) { pw.println("/**"); pw.println(" * Default implementation of DateTimeFormatInfo interface, " + "using values from"); pw.println(" * the CLDR root locale."); pw.println(" * <p>"); pw.println(" * Users who need to create their own DateTimeFormatInfo " + "implementation are"); pw.println(" * encouraged to extend this class so their implementation " + "won't break when "); pw.println(" * new methods are added."); pw.println(" */"); } else { pw.println("/**"); pw.println(" * Implementation of DateTimeFormatInfo for the \"" + locale + "\" locale."); pw.println(" */"); } pw.print("public class " + myClass); if (locale.isDefault()) { pw.print(" implements " + DateTimeFormatInfo.class.getSimpleName()); } else { pw.print(" extends "); pw.print(DateTimeFormatInfoImpl.class.getSimpleName()); if (!parent.isDefault()) { pw.print('_'); pw.print(parent.getAsString()); } } pw.println(" {"); // write AM/PM names generateStringList(pw, "dayPeriod-abbrev", null, locale, "ampms", "am", "pm"); // write standard date formats generateArgMethod(pw, "date", locale, "dateFormat"); generateStringMethod(pw, "date", locale, "full", "dateFormatFull"); generateStringMethod(pw, "date", locale, "long", "dateFormatLong"); generateStringMethod(pw, "date", locale, "medium", "dateFormatMedium"); generateStringMethod(pw, "date", locale, "short", "dateFormatShort"); // write methods for assembling date/time formats generateArgMethod(pw, "dateTime", locale, "dateTime", "timePattern", "datePattern"); generateArgMethodRedirect(pw, "dateTime", locale, "full", "dateTimeFull", "timePattern", "datePattern"); generateArgMethodRedirect(pw, "dateTime", locale, "long", "dateTimeLong", "timePattern", "datePattern"); generateArgMethodRedirect(pw, "dateTime", locale, "medium", "dateTimeMedium", "timePattern", "datePattern"); generateArgMethodRedirect(pw, "dateTime", locale, "short", "dateTimeShort", "timePattern", "datePattern"); // write era names generateStringList(pw, "era-wide", null, locale, "erasFull", "0", "1"); generateStringList(pw, "era-abbrev", null, locale, "erasShort", "0", "1"); // write firstDayOfTheWeek generateDayNumber(pw, locale, "firstDay", "firstDayOfTheWeek"); // write predefined date/time formats for (Map.Entry<String, String> entry : FORMAT_BY_METHOD.entrySet()) { generateFormat(locale, pw, entry.getValue(), entry.getKey()); } // write month names generateFullStringList(pw, "month", locale, "months", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"); // write quarter names generateStringList(pw, "quarter-wide", null, locale, "quartersFull", "1", "2", "3", "4"); generateStringList(pw, "quarter-abbrev", null, locale, "quartersShort", "1", "2", "3", "4"); // write standard time formats generateArgMethod(pw, "time", locale, "timeFormat"); generateStringMethod(pw, "time", locale, "full", "timeFormatFull"); generateStringMethod(pw, "time", locale, "long", "timeFormatLong"); generateStringMethod(pw, "time", locale, "medium", "timeFormatMedium"); generateStringMethod(pw, "time", locale, "short", "timeFormatShort"); // write weekday names generateFullStringList(pw, "day", locale, "weekdays", DAYS); // write weekend boundaries generateDayNumber(pw, locale, "weekendEnd", "weekendEnd"); generateDayNumber(pw, locale, "weekendStart", "weekendStart"); pw.println("}"); pw.close(); } } /** * @param period */ private void computePeriodRedirects(String period) { localeData.computeRedirects(period + "-abbrev", period + "-sa-abbrev"); localeData.computeRedirects(period + "-narrow", period + "-sa-narrow"); localeData.computeRedirects(period + "-wide", period + "-sa-wide"); } private void loadFormatPatterns() { localeData.addEntries("predef", cldrFactory, "//ldml/dates/calendars/calendar[@type=\"gregorian\"]/dateTimeFormats/" + "availableFormats", "dateFormatItem", "id"); for (GwtLocale locale : localeData.getAllLocales()) { DateTimePatternGenerator dtpg = new DateTimePatternGenerator(locale); for (Map.Entry<String, String> entry : FORMATS.entrySet()) { String skeleton = entry.getKey(); String cldrPattern = localeData.getEntry("predef", locale, skeleton); String pattern = dtpg.getBestPattern(skeleton); if (cldrPattern != null && !cldrPattern.equals(pattern)) { System.err.println("Mismatch on skeleton pattern in locale " + locale + " for skeleton '" + skeleton + "': icu='" + pattern + "', cldr='" + cldrPattern + "'"); } localeData.addEntry("predef", locale, skeleton, pattern); } } } /** * Load the week start and weekend range values from CLDR. */ private void loadWeekData() { localeData.addTerritoryEntries("weekdata", cldrFactory, regionLanguageData, "//supplementalData/weekData/firstDay", "firstDay", "day"); localeData.addTerritoryEntries("weekdata", cldrFactory, regionLanguageData, "//supplementalData/weekData/weekendStart", "weekendStart", "day"); localeData.addTerritoryEntries("weekdata", cldrFactory, regionLanguageData, "//supplementalData/weekData/weekendEnd", "weekendEnd", "day"); localeData.addTerritoryEntries("weekdata", cldrFactory, regionLanguageData, "//supplementalData/weekData/minDays", "minDays", "count"); } /** * Remove duplicates from period names. * * @param group */ private void removePeriodDuplicates(String group) { removePeriodWidthDuplicates(group, "wide"); removePeriodWidthDuplicates(group, "abbrev"); removePeriodWidthDuplicates(group, "narrow"); } private void removePeriodWidthDuplicates(String group, String width) { localeData.removeCompleteDuplicates(group + "-" + width); localeData.removeCompleteDuplicates(group + "-sa-" + width); localeData.removeCompleteDuplicates(group + "-sa-" + width + "-redirect"); } private void removeUnusedFormats() { for (GwtLocale locale : localeData.getAllLocales()) { Set<String> toRemove = new HashSet<String>(); Map<String, String> map = localeData.getEntries("predef", locale); for (Entry<String, String> entry : map.entrySet()) { if (!FORMATS.containsKey(entry.getKey())) { toRemove.add(entry.getKey()); } } localeData.removeEntries("predef", locale, toRemove); } } }