/* * 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.datetimefmtcreator; import com.google.gwt.i18n.client.constants.DateTimeConstantsImpl; import com.google.gwt.i18n.rebind.LocaleUtils; import com.google.gwt.i18n.shared.GwtLocale; import com.google.gwt.i18n.shared.GwtLocaleFactory; import com.ibm.icu.text.DateTimePatternGenerator; import com.ibm.icu.util.ULocale; import org.apache.tapestry.util.text.LocalizedProperties; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.PrintWriter; import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.Map.Entry; import java.util.regex.Pattern; /** * Generate implementations of DateTimeFormatInfoImpl for all the supported * locales. */ public class DateTimeFormatCreator { private static class DtfiGenerator { private static void buildPatterns(GwtLocale locale, TreeMap<Key, String[]> properties) { ULocale ulocale = new ULocale(ULocale.canonicalize(locale.getAsString())); DateTimePatternGenerator dtpg = DateTimePatternGenerator.getInstance(ulocale); for (Map.Entry<String, String> entry : patterns.entrySet()) { properties.put(new Key(locale, "format" + entry.getKey()), new String[] {dtpg .getBestPattern(entry.getValue())}); } } private static GwtLocale findEarliestAncestor(GwtLocale locale, Set<GwtLocale> set) { if (set == null) { return null; } for (GwtLocale search : locale.getInheritanceChain()) { if (set.contains(search)) { return search; } } return null; } private static String quote(String value) { return value.replaceAll("\"", "\\\\\""); } private static String[] split(String target) { // We add an artificial end character to avoid the odd split() behavior // that drops the last item if it is only whitespace. target = target + "~"; // Do not split on escaped commas. String[] args = target.split("(?<![\\\\]),"); // Now remove the artificial ending we added above. // We have to do it before we escape and trim because otherwise // the artificial trailing '~' would prevent the last item from being // properly trimmed. if (args.length > 0) { int last = args.length - 1; args[last] = args[last].substring(0, args[last].length() - 1); } for (int i = 0; i < args.length; i++) { args[i] = args[i].replaceAll("\\\\,", ",").trim(); } return args; } private File propDir; private File src; public DtfiGenerator(File src) { this.src = src; String packageName = DateTimeConstantsImpl.class.getPackage().getName(); propDir = new File(src, packageName.replaceAll("\\.", "/")); if (!propDir.exists()) { System.err.println("Can't find directory for " + packageName); return; } } public void generate() throws FileNotFoundException, IOException { final Pattern dtcProps = Pattern.compile("DateTimeConstantsImpl(.*)\\.properties"); String[] propFiles = propDir.list(new FilenameFilter() { public boolean accept(File dir, String name) { return dtcProps.matcher(name).matches(); } }); TreeMap<Key, String[]> properties = new TreeMap<Key, String[]>(); GwtLocaleFactory factory = LocaleUtils.getLocaleFactory(); collectPropertyData(propFiles, properties, factory); Map<GwtLocale, Set<GwtLocale>> parents = removeInheritedValues(properties); generateSources(properties, parents); } private void addLocaleParent(Map<GwtLocale, Set<GwtLocale>> parents, GwtLocale keyLocale, GwtLocale parentLocale) { Set<GwtLocale> parentSet = parents.get(keyLocale); if (parentSet == null) { parentSet = new HashSet<GwtLocale>(); parents.put(keyLocale, parentSet); } parentSet.add(parentLocale); } @SuppressWarnings("unchecked") private void collectPropertyData(String[] propFiles, TreeMap<Key, String[]> properties, GwtLocaleFactory factory) throws FileNotFoundException, IOException { for (String propFile : propFiles) { if (!propFile.startsWith("DateTimeConstantsImpl") || !propFile.endsWith(".properties")) { continue; } int len = propFile.length(); String suffix = propFile.substring(21, len - 11); if (suffix.startsWith("_")) { suffix = suffix.substring(1); } GwtLocale locale = factory.fromString(suffix).getCanonicalForm(); File f = new File(propDir, propFile); FileInputStream str = null; try { str = new FileInputStream(f); LocalizedProperties props = new LocalizedProperties(); props.load(str); Map<String, String> map = props.getPropertyMap(); for (Map.Entry<String, String> entry : map.entrySet()) { String[] value = split(entry.getValue()); if ("dateFormats".equals(entry.getKey()) || "timeFormats".equals(entry.getKey()) || "weekendRange".equals(entry.getKey())) { // split these out into separate fields for (int i = 0; i < value.length; ++i) { Key key = new Key(locale, entry.getKey() + i); properties.put(key, new String[] {value[i]}); } } else { Key key = new Key(locale, entry.getKey()); properties.put(key, value); } } buildPatterns(locale, properties); } finally { if (str != null) { str.close(); } } } } private PrintWriter createClassSource(String packageName, String className) throws FileNotFoundException { String path = packageName.replace('.', '/') + "/" + className + ".java"; File f = new File(src, path); FileOutputStream ostr = new FileOutputStream(f); PrintWriter out = new PrintWriter(ostr); out.println("/*"); out.println(" * Copyright 2010 Google Inc."); out.println(" * "); out.println(" * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not"); out.println(" * use this file except in compliance with the License. You may obtain a copy of"); out.println(" * the License at"); out.println(" * "); out.println(" * http://www.apache.org/licenses/LICENSE-2.0"); out.println(" *"); out.println(" * Unless required by applicable law or agreed to in writing, software"); out.println(" * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT"); out.println(" * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the"); out.println(" * License for the specific language governing permissions and limitations under"); out.println(" * the License."); out.println(" */"); out.println("package " + packageName + ";"); out.println(); out.println("// DO NOT EDIT - GENERATED FROM CLDR DATA"); out.println(); return out; } private void generateAlias(GwtLocale locale, GwtLocale parent) throws IOException { System.out.println("Generating alias " + locale); String suffix; if (parent.isDefault()) { suffix = ""; } else { suffix = "_" + parent.getAsString(); } String packageName = "com.google.gwt.i18n.client.impl.cldr"; String className = "DateTimeFormatInfoImpl_" + locale.getAsString(); PrintWriter out = null; try { out = createClassSource(packageName, className); out.println("/**"); out.println(" * Locale \"" + locale + "\" is an alias for \"" + parent + "\"."); out.println(" */"); out.println("public class " + className + " extends DateTimeFormatInfoImpl" + suffix + " {"); out.println("}"); } finally { if (out != null) { out.close(); } } } private void generateLocale(GwtLocale locale, GwtLocale parent, Map<String, String[]> values) throws IOException { System.out.println("Generating locale " + locale); boolean addOverrides = true; PrintWriter out = null; try { if (locale.isDefault()) { String packageName = "com.google.gwt.i18n.client"; String className = "DefaultDateTimeFormatInfo"; out = createClassSource(packageName, className); out.println("/**"); out.println(" * Default implementation of DateTimeFormatInfo interface, using values from"); out.println(" * the CLDR root locale."); out.println(" * <p>"); out.println(" * Users who need to create their own DateTimeFormatInfo implementation are"); out.println(" * encouraged to extend this class so their implementation won't break when"); out.println(" * new methods are added."); out.println(" */"); out.println("public class DefaultDateTimeFormatInfo implements DateTimeFormatInfo {"); addOverrides = false; } else { String suffix; if (parent.isDefault()) { suffix = ""; } else { suffix = "_" + parent.getAsString(); } String packageName = "com.google.gwt.i18n.client.impl.cldr"; String className = "DateTimeFormatInfoImpl_" + locale.getAsString(); out = createClassSource(packageName, className); out.println("/**"); out.println(" * Implementation of DateTimeFormatInfo for locale \"" + locale + "\"."); out.println(" */"); out.println("public class " + className + " extends DateTimeFormatInfoImpl" + suffix + " {"); } Set<String> keySet = values.keySet(); String[] keys = keySet.toArray(new String[keySet.size()]); Arrays.sort(keys, new Comparator<String>() { public int compare(String a, String b) { String mappedA = a; String mappedB = b; FieldMapping field = fieldMap.get(a); if (field != null) { mappedA = field.methodName; } field = fieldMap.get(b); if (field != null) { mappedB = field.methodName; } return mappedA.compareTo(mappedB); } }); for (String key : keys) { String[] value = values.get(key); FieldMapping mapping = fieldMap.get(key); Class<?> type = value.length > 1 ? String[].class : String.class; String related = null; String name = key; if (mapping != null) { name = mapping.methodName; type = mapping.type; related = mapping.related; } String[] relatedValue = values.get(related); String relayMethod = null; if (Arrays.equals(value, relatedValue)) { relayMethod = fieldMap.get(related).methodName; } out.println(); if (addOverrides) { out.println(" @Override"); } out.println(" public " + type.getSimpleName() + " " + name + "() {"); out.print(" return "); if (relayMethod != null) { out.println(relayMethod + "();"); } else { if (type.isArray()) { out.println("new " + type.getSimpleName() + " { "); out.print(" "); } boolean first = true; for (String oneValue : value) { if (!first) { out.println(","); out.print(" "); } if (type == int.class || type == int[].class) { out.print(Integer.valueOf(oneValue) - 1); } else { out.print("\"" + quote(oneValue) + "\""); } first = false; } if (type.isArray()) { out.println(); out.println(" };"); } else { out.println(";"); } } out.println(" }"); } if (locale.isDefault()) { // TODO(jat): actually generate these from CLDR data out.println(); out.println(" public String dateFormat() {"); out.println(" return dateFormatMedium();"); out.println(" }"); out.println(); out.println(" public String dateTime(String timePattern, String datePattern) {"); out.println(" return datePattern + \" \" + timePattern;"); out.println(" }"); out.println(); out.println(" public String dateTimeFull(String timePattern, String datePattern) {"); out.println(" return dateTime(timePattern, datePattern);"); out.println(" }"); out.println(); out.println(" public String dateTimeLong(String timePattern, String datePattern) {"); out.println(" return dateTime(timePattern, datePattern);"); out.println(" }"); out.println(); out.println(" public String dateTimeMedium(String timePattern, String datePattern) {"); out.println(" return dateTime(timePattern, datePattern);"); out.println(" }"); out.println(); out.println(" public String dateTimeShort(String timePattern, String datePattern) {"); out.println(" return dateTime(timePattern, datePattern);"); out.println(" }"); out.println(); out.println(" public String timeFormat() {"); out.println(" return timeFormatMedium();"); out.println(" }"); } out.println("}"); } finally { if (out != null) { out.close(); } } } private void generateSources(TreeMap<Key, String[]> properties, Map<GwtLocale, Set<GwtLocale>> parents) throws IOException { Set<GwtLocale> locales = new HashSet<GwtLocale>(); // process sorted locales/keys, generating each locale on change GwtLocale lastLocale = null; Map<String, String[]> thisLocale = new HashMap<String, String[]>(); for (Entry<Key, String[]> entry : properties.entrySet()) { if (lastLocale != null && lastLocale != entry.getKey().locale) { GwtLocale parent = findEarliestAncestor(lastLocale, parents.get(lastLocale)); generateLocale(lastLocale, parent, thisLocale); thisLocale.clear(); lastLocale = null; } if (lastLocale == null) { lastLocale = entry.getKey().locale; locales.add(lastLocale); } thisLocale.put(entry.getKey().key, entry.getValue()); } if (lastLocale != null) { GwtLocale parent = findEarliestAncestor(lastLocale, parents.get(lastLocale)); generateLocale(lastLocale, parent, thisLocale); } Set<GwtLocale> seen = new HashSet<GwtLocale>(locales); for (GwtLocale locale : locales) { for (GwtLocale alias : locale.getAliases()) { if (!seen.contains(alias)) { seen.add(alias); // generateAlias(alias, locale); } } } } /** * Check if a given entry within a locale is inherited from a parent. * * @param properties * @param parents * @param key * @param value * @return true if the value is the same as the first parent defining that * value */ private boolean isInherited(TreeMap<Key, String[]> properties, Map<GwtLocale, Set<GwtLocale>> parents, Key key, String[] value) { GwtLocale keyLocale = key.locale; if (keyLocale.isDefault()) { // never delete entries from default return false; } List<GwtLocale> list = keyLocale.getInheritanceChain(); String[] parent = null; for (int i = 1; i < list.size(); ++i) { Key parentKey = new Key(list.get(i), key.key); parent = properties.get(parentKey); if (parent != null) { GwtLocale parentLocale = parentKey.locale; addLocaleParent(parents, keyLocale, parentLocale); break; } } return Arrays.equals(value, parent); } /** * Remove inherited values and return a map of inherited-from locales for * each locale. * * @param properties * @return inheritance map */ private Map<GwtLocale, Set<GwtLocale>> removeInheritedValues(TreeMap<Key, String[]> properties) { // remove entries identical to a parent locale Map<GwtLocale, Set<GwtLocale>> parents = new HashMap<GwtLocale, Set<GwtLocale>>(); Set<Entry<Key, String[]>> entrySet = properties.entrySet(); Iterator<Entry<Key, String[]>> it = entrySet.iterator(); while (it.hasNext()) { Entry<Key, String[]> entry = it.next(); if (isInherited(properties, parents, entry.getKey(), entry.getValue())) { it.remove(); } } return parents; } } private static class FieldMapping { public final String methodName; public final Class<?> type; public final String related; public FieldMapping(String methodName, Class<?> type) { this(methodName, type, null); } public FieldMapping(String methodName, Class<?> type, String related) { this.methodName = methodName; this.type = type; this.related = related; } } private static class Key implements Comparable<Key> { public final GwtLocale locale; public final String key; public Key(GwtLocale locale, String key) { this.locale = locale; this.key = key; } public int compareTo(Key other) { int c = locale.compareTo(other.locale); if (c == 0) { c = key.compareTo(other.key); } return c; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } Key other = (Key) obj; return locale.equals(other.locale) && key.equals(other.key); } @Override public int hashCode() { return locale.hashCode() * 31 + key.hashCode(); } @Override public String toString() { return locale.toString() + "/" + key; } } private static final Map<String, FieldMapping> fieldMap = new HashMap<String, FieldMapping>(); private static Map<String, String> patterns = new HashMap<String, String>(); static { fieldMap.put("ampms", new FieldMapping("ampms", String[].class)); fieldMap.put("dateFormats0", new FieldMapping("dateFormatFull", String.class)); fieldMap.put("dateFormats1", new FieldMapping("dateFormatLong", String.class)); fieldMap.put("dateFormats2", new FieldMapping("dateFormatMedium", String.class)); fieldMap.put("dateFormats3", new FieldMapping("dateFormatShort", String.class)); fieldMap.put("timeFormats0", new FieldMapping("timeFormatFull", String.class)); fieldMap.put("timeFormats1", new FieldMapping("timeFormatLong", String.class)); fieldMap.put("timeFormats2", new FieldMapping("timeFormatMedium", String.class)); fieldMap.put("timeFormats3", new FieldMapping("timeFormatShort", String.class)); fieldMap.put("eraNames", new FieldMapping("erasFull", String[].class)); fieldMap.put("eras", new FieldMapping("erasShort", String[].class)); fieldMap.put("quarters", new FieldMapping("quartersFull", String[].class)); fieldMap.put("shortQuarters", new FieldMapping("quartersShort", String[].class)); fieldMap.put("firstDayOfTheWeek", new FieldMapping("firstDayOfTheWeek", Integer.class)); fieldMap.put("months", new FieldMapping("monthsFull", String[].class)); fieldMap.put("standaloneMonths", new FieldMapping("monthsFullStandalone", String[].class, "months")); fieldMap.put("narrowMonths", new FieldMapping("monthsNarrow", String[].class)); fieldMap.put("standaloneNarrowMonths", new FieldMapping("monthsNarrowStandalone", String[].class, "narrowMonths")); fieldMap.put("shortMonths", new FieldMapping("monthsShort", String[].class)); fieldMap.put("standaloneShortMonths", new FieldMapping("monthsShortStandalone", String[].class, "shortMonths")); fieldMap.put("weekendRange0", new FieldMapping("weekendStart", int.class)); fieldMap.put("weekendRange1", new FieldMapping("weekendEnd", int.class)); fieldMap.put("firstDayOfTheWeek", new FieldMapping("firstDayOfTheWeek", int.class)); fieldMap.put("weekdays", new FieldMapping("weekdaysFull", String[].class)); fieldMap.put("standaloneWeekdays", new FieldMapping("weekdaysFullStandalone", String[].class, "weekdays")); fieldMap.put("shortWeekdays", new FieldMapping("weekdaysShort", String[].class)); fieldMap.put("standaloneShortWeekdays", new FieldMapping("weekdaysShortStandalone", String[].class, "shortWeekdays")); fieldMap.put("narrowWeekdays", new FieldMapping("weekdaysNarrow", String[].class)); fieldMap.put("standaloneNarrowWeekdays", new FieldMapping("weekdaysNarrowStandalone", String[].class, "narrowWeekdays")); // patterns to use with DateTimePatternGenerator patterns.put("Day", "d"); patterns.put("Hour12Minute", "hmm"); patterns.put("Hour12MinuteSecond", "hmmss"); patterns.put("Hour24Minute", "Hmm"); patterns.put("Hour24MinuteSecond", "Hmmss"); patterns.put("MinuteSecond", "mss"); patterns.put("MonthAbbrev", "MMM"); patterns.put("MonthAbbrevDay", "MMMd"); patterns.put("MonthFull", "MMMM"); patterns.put("MonthFullDay", "MMMMd"); patterns.put("MonthFullWeekdayDay", "MMMMEEEEd"); patterns.put("MonthNumDay", "Md"); patterns.put("Year", "y"); patterns.put("YearMonthAbbrev", "yMMM"); patterns.put("YearMonthAbbrevDay", "yMMMd"); patterns.put("YearMonthFull", "yMMMM"); patterns.put("YearMonthFullDay", "yMMMMd"); patterns.put("YearMonthNum", "yM"); patterns.put("YearMonthNumDay", "yMd"); patterns.put("YearMonthWeekdayDay", "yMMMEEEd"); patterns.put("YearQuarterFull", "yQQQQ"); patterns.put("YearQuarterShort", "yQ"); } /** * @param args * @throws IOException */ public static void main(String[] args) throws IOException { if (args.length != 1) { System.err.println("Usage: " + DateTimeFormatCreator.class.getSimpleName() + " gwt-root-dir"); return; } File gwt = new File(args[0]); File src = new File(gwt, "user/src"); if (!gwt.exists() || !src.exists()) { System.err.println(args[0] + " doesn't appear to be a GWT root directory"); return; } new DtfiGenerator(src).generate(); } }