From 9a15ff31a284016106d94a7be648071726952238 Mon Sep 17 00:00:00 2001 From: Mark Davis Date: Thu, 24 Nov 2005 09:32:39 +0000 Subject: [PATCH] ICU-4700 more cldr tools X-SVN-Rev: 18830 --- .../format/GlobalizationPreferencesTest.java | 30 +- .../ibm/icu/dev/test/util/BagFormatter.java | 69 --- .../test/util/TransliteratorUtilities.java | 76 +++ .../icu/dev/test/util/UnicodeProperty.java | 46 +- .../com/ibm/icu/impl/CollectionUtilities.java | 43 +- icu4j/src/com/ibm/icu/impl/PrettyPrinter.java | 1 + icu4j/src/com/ibm/icu/text/UnicodeSet.java | 2 +- icu4j/src/com/ibm/icu/util/Freezable.java | 307 +++++++++++- .../icu/util/GlobalizationPreferences.java | 465 ++++++++++++++---- 9 files changed, 818 insertions(+), 221 deletions(-) diff --git a/icu4j/src/com/ibm/icu/dev/test/format/GlobalizationPreferencesTest.java b/icu4j/src/com/ibm/icu/dev/test/format/GlobalizationPreferencesTest.java index 6e64f8ac1d4..052a0133e93 100644 --- a/icu4j/src/com/ibm/icu/dev/test/format/GlobalizationPreferencesTest.java +++ b/icu4j/src/com/ibm/icu/dev/test/format/GlobalizationPreferencesTest.java @@ -9,6 +9,7 @@ package com.ibm.icu.dev.test.format; import java.io.IOException; +import java.io.PrintStream; import java.io.PrintWriter; import java.util.Arrays; import java.util.Date; @@ -35,7 +36,8 @@ public class GlobalizationPreferencesTest { private static final String[] WidthNames = {"abbreviated", "wide", "narrow"}; public static void main(String[] args) throws IOException { - PrintWriter out = BagFormatter.openUTF8Writer("c:/", "tempFile.txt"); + PrintStream out = System.out; + //PrintWriter out = BagFormatter.openUTF8Writer("c:/", "tempFile.txt"); try { Date now = new Date(); @@ -49,13 +51,13 @@ public class GlobalizationPreferencesTest { out.println("Check defaulting"); String[] localeList = {"fr_BE;q=0.5,de", "fr_BE,de", "fr", "en_NZ", "en", "en-TH", "zh-Hant", "zh-MO", "zh", "it", "as", "haw", "ar-EG", "ar", "qqq"}; for (int i = 0; i < localeList.length; ++i) { - lPreferences.setULocales(localeList[i]); + lPreferences.setLocales(localeList[i]); out.println("\tdefaults for: \t" + localeList[i] + "\t" - + lPreferences.getULocales() + + lPreferences.getLocales() + ", \t" + lPreferences.getTerritory() + ", \t" + lPreferences.getCurrency() + ", \t" + lPreferences.getCalendar().getClass() - + ", \t" + lPreferences.getTimezone().getID() + + ", \t" + lPreferences.getTimeZone().getID() ); } @@ -66,13 +68,17 @@ public class GlobalizationPreferencesTest { out.println("\tdate: \t" + lPreferences.getDateFormat(DateFormat.FULL, GlobalizationPreferences.NONE).format(now)); out.println("setting locale to Germany"); - lPreferences.setULocales(ULocale.GERMANY); + lPreferences.setLocale(ULocale.GERMANY); out.println("\tdate: \t" + lPreferences.getDateFormat(DateFormat.FULL, GlobalizationPreferences.NONE).format(now)); out.println("setting date locale to France"); lPreferences.setDateLocale(ULocale.FRANCE); out.println("\tdate: \t" + lPreferences.getDateFormat(DateFormat.FULL, GlobalizationPreferences.NONE).format(now)); + out.println("setting explicit pattern"); + lPreferences.setDateFormat(DateFormat.FULL, GlobalizationPreferences.NONE, "GGG yyyy+MMM+DD vvvv"); + out.println("\tdate: \t" + lPreferences.getDateFormat(DateFormat.FULL, GlobalizationPreferences.NONE).format(now)); + out.println("setting date format to yyyy-MMM-dd (Italy)"); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MMM-dd",ULocale.ITALY); lPreferences.setDateFormat(DateFormat.FULL, GlobalizationPreferences.NONE, sdf); @@ -111,11 +117,21 @@ public class GlobalizationPreferencesTest { lPreferences.setNumberLocale(new ULocale("hi-IN")); out.println("\tcurrency: \t" + lPreferences.getNumberFormat(GlobalizationPreferences.CURRENCY).format(1234.567)); + out.println(); + out.println("Comparison"); + out.println("setting number locale to Germany"); + lPreferences.setLocale(ULocale.GERMANY); + out.println("\tcompare: \u00e4 & z \t" + lPreferences.getCollator().compare("\u00e4", "z")); + + out.println("setting number locale to Swedish"); + lPreferences.setLocale(new ULocale("sv")); + out.println("\tcompare: \u00e4 & z \t" + lPreferences.getCollator().compare("\u00e4", "z")); + // now try a fallback within locales out.println(); out.println("Display Names"); - lPreferences.setULocales(new ULocale[]{new ULocale("as"),new ULocale("pl"),new ULocale("fr")}); - out.println("Trying fallback for multiple locales: " + lPreferences.getULocales()); + lPreferences.setLocales(new ULocale[]{new ULocale("as"),new ULocale("pl"),new ULocale("fr")}); + out.println("Trying fallback for multiple locales: " + lPreferences.getLocales()); String[][] testItems = { {GlobalizationPreferences.LOCALEID+"", "as_FR", "en_RU","haw_CA","se_Cyrl_AT"}, {GlobalizationPreferences.LANGUAGEID+"", "as", "en","haw","se","kok"}, diff --git a/icu4j/src/com/ibm/icu/dev/test/util/BagFormatter.java b/icu4j/src/com/ibm/icu/dev/test/util/BagFormatter.java index f263fcce085..95b8677fd64 100644 --- a/icu4j/src/com/ibm/icu/dev/test/util/BagFormatter.java +++ b/icu4j/src/com/ibm/icu/dev/test/util/BagFormatter.java @@ -42,75 +42,6 @@ public class BagFormatter { SHOW_FILES = showFiles; } - private static final String BASE_RULES = - "'<' > '<' ;" + - "'<' < '&'[lL][Tt]';' ;" + - "'&' > '&' ;" + - "'&' < '&'[aA][mM][pP]';' ;" + - "'>' < '&'[gG][tT]';' ;" + - "'\"' < '&'[qQ][uU][oO][tT]';' ; " + - "'' < '&'[aA][pP][oO][sS]';' ; "; - - private static final String CONTENT_RULES = - "'>' > '>' ;"; - - private static final String HTML_RULES = BASE_RULES + CONTENT_RULES + - "'\"' > '"' ; "; - - private static final String HTML_RULES_CONTROLS = HTML_RULES + - "([[:C:][:Z:][:whitespace:][:Default_Ignorable_Code_Point:]]) > &hex/unicode($1) ; "; - - private static final String XML_RULES = HTML_RULES + - "'' > ''' ; "; - - /* -The ampersand character (&) and the left angle bracket (<) MUST NOT appear - -in their literal form, except when used as markup delimiters, or within a - -comment, a processing instruction, or a CDATA section. If they are needed - -elsewhere, they MUST be escaped using either numeric character references or - -the strings "&" and "<" respectively. The right angle bracket (>) MAY - -be represented using the string ">", and MUST, for compatibility, be - -escaped using either ">" or a character reference when it appears in the string - -"]]>" in content, when that string is not marking the end of a CDATA section. - -In the content of elements, character data is any string of characters which does - -not contain the start-delimiter of any markup and does not include the - -CDATA-section-close delimiter, "]]>". In a CDATA section, character data is - -any string of characters not including the CDATA-section-close delimiter, - -"]]>". - -To allow attribute values to contain both single and double quotes, the - -apostrophe or single-quote character (') MAY be represented as "'", and - -the double-quote character (") as """. - - - */ - - public static final Transliterator toXML = Transliterator.createFromRules( - "any-xml", XML_RULES, Transliterator.FORWARD); - public static final Transliterator fromXML = Transliterator.createFromRules( - "xml-any", XML_RULES, Transliterator.REVERSE); - - public static final Transliterator toHTML = Transliterator.createFromRules( - "any-html", HTML_RULES, Transliterator.FORWARD); - public static final Transliterator toHTMLControl = Transliterator.createFromRules( - "any-html", HTML_RULES_CONTROLS, Transliterator.FORWARD); - public static final Transliterator fromHTML = Transliterator.createFromRules( - "html-any", HTML_RULES, Transliterator.REVERSE); - public static final PrintWriter CONSOLE = new PrintWriter(System.out,true); private static PrintWriter log = CONSOLE; diff --git a/icu4j/src/com/ibm/icu/dev/test/util/TransliteratorUtilities.java b/icu4j/src/com/ibm/icu/dev/test/util/TransliteratorUtilities.java index 38e5f8352b0..af60380ac78 100644 --- a/icu4j/src/com/ibm/icu/dev/test/util/TransliteratorUtilities.java +++ b/icu4j/src/com/ibm/icu/dev/test/util/TransliteratorUtilities.java @@ -61,4 +61,80 @@ public class TransliteratorUtilities { br.close(); return buffer.toString(); } + + private static final String BASE_RULES = + ":: (hex-any/xml);" + + ":: (hex-any/xml10);" + + "'<' > '<' ;" + + "'<' < '&'[lL][Tt]';' ;" + + "'&' > '&' ;" + + "'&' < '&'[aA][mM][pP]';' ;" + + "'>' < '&'[gG][tT]';' ;" + + "'\"' < '&'[qQ][uU][oO][tT]';' ; " + + "'' < '&'[aA][pP][oO][sS]';' ; "; + + private static final String CONTENT_RULES = + "'>' > '>' ;"; + + private static final String HTML_RULES = BASE_RULES + CONTENT_RULES + + "'\"' > '"' ; "; + + private static final String HTML_RULES_CONTROLS = HTML_RULES + + ":: [[:C:][:Z:][:whitespace:][:Default_Ignorable_Code_Point:]] hex/unicode ; "; + + private static final String HTML_RULES_ASCII = HTML_RULES + + ":: [[:C:][:^ASCII:]] any-hex/xml ; "; + + private static final String XML_RULES = HTML_RULES + + "'' > ''' ; " +; + + /* +The ampersand character (&) and the left angle bracket (<) MUST NOT appear + +in their literal form, except when used as markup delimiters, or within a + +comment, a processing instruction, or a CDATA section. If they are needed + +elsewhere, they MUST be escaped using either numeric character references or + +the strings "&" and "<" respectively. The right angle bracket (>) MAY + +be represented using the string ">", and MUST, for compatibility, be + +escaped using either ">" or a character reference when it appears in the string + +"]]>" in content, when that string is not marking the end of a CDATA section. + +In the content of elements, character data is any string of characters which does + +not contain the start-delimiter of any markup and does not include the + +CDATA-section-close delimiter, "]]>". In a CDATA section, character data is + +any string of characters not including the CDATA-section-close delimiter, + +"]]>". + +To allow attribute values to contain both single and double quotes, the + +apostrophe or single-quote character (') MAY be represented as "'", and + +the double-quote character (") as """. + + + */ + + public static final Transliterator toXML = Transliterator.createFromRules( + "any-xml", XML_RULES, Transliterator.FORWARD); + public static final Transliterator fromXML = Transliterator.createFromRules( + "xml-any", XML_RULES, Transliterator.REVERSE); + public static final Transliterator toHTML = Transliterator.createFromRules( + "any-html", HTML_RULES, Transliterator.FORWARD); + public static final Transliterator toHTMLControl = Transliterator.createFromRules( + "any-html", HTML_RULES_CONTROLS, Transliterator.FORWARD); + public static final Transliterator toHTMLAscii = Transliterator.createFromRules( + "any-html", HTML_RULES_ASCII, Transliterator.FORWARD); + public static final Transliterator fromHTML = Transliterator.createFromRules( + "html-any", HTML_RULES, Transliterator.REVERSE); } \ No newline at end of file diff --git a/icu4j/src/com/ibm/icu/dev/test/util/UnicodeProperty.java b/icu4j/src/com/ibm/icu/dev/test/util/UnicodeProperty.java index 0477345d109..7434c74706c 100644 --- a/icu4j/src/com/ibm/icu/dev/test/util/UnicodeProperty.java +++ b/icu4j/src/com/ibm/icu/dev/test/util/UnicodeProperty.java @@ -20,6 +20,8 @@ import java.util.TreeMap; import java.util.regex.Pattern; import com.ibm.icu.impl.Utility; +import com.ibm.icu.impl.CollectionUtilities.InverseMatcher; +import com.ibm.icu.impl.CollectionUtilities.ObjectMatcher; import com.ibm.icu.text.SymbolTable; import com.ibm.icu.text.UTF16; import com.ibm.icu.text.UnicodeMatcher; @@ -215,7 +217,7 @@ Name: Unicode_1_Name public final UnicodeSet getSet(String propertyValue) { return getSet(propertyValue,null); } - public final UnicodeSet getSet(Matcher matcher) { + public final UnicodeSet getSet(PatternMatcher matcher) { return getSet(matcher,null); } @@ -229,7 +231,7 @@ Name: Unicode_1_Name public static final String UNUSED = "??"; - public final UnicodeSet getSet(Matcher matcher, UnicodeSet result) { + public final UnicodeSet getSet(PatternMatcher matcher, UnicodeSet result) { if (result == null) result = new UnicodeSet(); if (isType(STRING_OR_MISC_MASK)) { for (int i = 0; i <= 0x10FFFF; ++i) { @@ -605,12 +607,12 @@ Name: Unicode_1_Name } return result; } - InverseMatcher inverseMatcher = new InverseMatcher(); + InversePatternMatcher inverseMatcher = new InversePatternMatcher(); /** * Format is: * propname ('=' | '!=') propvalue ( '|' propValue )* */ - public final UnicodeSet getSet(String propAndValue, Matcher matcher, UnicodeSet result) { + public final UnicodeSet getSet(String propAndValue, PatternMatcher matcher, UnicodeSet result) { int equalPos = propAndValue.indexOf('='); String prop = propAndValue.substring(0,equalPos); String value = propAndValue.substring(equalPos+1); @@ -632,7 +634,7 @@ Name: Unicode_1_Name return up.getSet(matcher.set(value), result); } - public final UnicodeSet getSet(String propAndValue, Matcher matcher) { + public final UnicodeSet getSet(String propAndValue, PatternMatcher matcher) { return getSet(propAndValue, matcher, null); } public final UnicodeSet getSet(String propAndValue) { @@ -855,57 +857,51 @@ Name: Unicode_1_Name } } - public interface Matcher { - /** - * Must be able to handle null - * @param value - * @return true if the value matches - */ - public boolean matches(String value); - public Matcher set(String pattern); + public interface PatternMatcher extends ObjectMatcher { + public PatternMatcher set(String pattern); } - public static class InverseMatcher implements Matcher { - Matcher other; - public Matcher set(Matcher toInverse) { + public static class InversePatternMatcher extends InverseMatcher implements PatternMatcher { + PatternMatcher other; + public PatternMatcher set(PatternMatcher toInverse) { other = toInverse; return this; } - public boolean matches(String value) { + public boolean matches(Object value) { return !other.matches(value); } - public Matcher set(String pattern) { + public PatternMatcher set(String pattern) { other.set(pattern); return this; } } - public static class SimpleMatcher implements Matcher { + public static class SimpleMatcher implements PatternMatcher { Comparator comparator; String pattern; public SimpleMatcher(String pattern, Comparator comparator) { this.comparator = comparator; this.pattern = pattern; } - public boolean matches(String value) { + public boolean matches(Object value) { if (comparator == null) return pattern.equals(value); return comparator.compare(pattern, value) == 0; } - public Matcher set(String pattern) { + public PatternMatcher set(String pattern) { this.pattern = pattern; return this; } } - public static class RegexMatcher implements UnicodeProperty.Matcher { + public static class RegexMatcher implements UnicodeProperty.PatternMatcher { private java.util.regex.Matcher matcher; - public UnicodeProperty.Matcher set(String pattern) { + public UnicodeProperty.PatternMatcher set(String pattern) { matcher = Pattern.compile(pattern).matcher(""); return this; } - public boolean matches(String value) { - matcher.reset(value); + public boolean matches(Object value) { + matcher.reset(value.toString()); return matcher.matches(); } } diff --git a/icu4j/src/com/ibm/icu/impl/CollectionUtilities.java b/icu4j/src/com/ibm/icu/impl/CollectionUtilities.java index 0ef60483a6a..fa19e146bcb 100644 --- a/icu4j/src/com/ibm/icu/impl/CollectionUtilities.java +++ b/icu4j/src/com/ibm/icu/impl/CollectionUtilities.java @@ -13,9 +13,11 @@ import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.SortedSet; + //#ifndef FOUNDATION import java.util.regex.Matcher; //#endif + import com.ibm.icu.text.Transliterator; import com.ibm.icu.text.UTF16; import com.ibm.icu.text.UnicodeSet; @@ -25,6 +27,27 @@ import com.ibm.icu.text.UnicodeSetIterator; * Utilities that ought to be on collections, but aren't */ public final class CollectionUtilities { + + public static String join(Object[] array, String separator) { + StringBuffer result = new StringBuffer(); + for (int i = 0; i < array.length; ++i) { + if (i != 0) result.append(separator); + result.append(array[i]); + } + return result.toString(); + } + + public static String join(Collection collection, String separator) { + StringBuffer result = new StringBuffer(); + boolean first = true; + for (Iterator it = collection.iterator(); it.hasNext();) { + if (first) first = false; + else result.append(separator); + result.append(it.next()); + } + return result.toString(); + } + /** * Utility like Arrays.asList() */ @@ -90,11 +113,25 @@ public final class CollectionUtilities { return bestSoFar; } - public interface Filter { + public interface ObjectMatcher { + /** + * Must handle null, never throw exception + */ boolean matches(Object o); } + + public static class InverseMatcher implements ObjectMatcher { + ObjectMatcher other; + public ObjectMatcher set(ObjectMatcher toInverse) { + other = toInverse; + return this; + } + public boolean matches(Object value) { + return !other.matches(value); + } + } - public static Collection removeAll(Collection c, Filter f) { + public static Collection removeAll(Collection c, ObjectMatcher f) { for (Iterator it = c.iterator(); it.hasNext();) { Object item = it.next(); if (f.matches(item)) it.remove(); @@ -102,7 +139,7 @@ public final class CollectionUtilities { return c; } - public static Collection retainAll(Collection c, Filter f) { + public static Collection retainAll(Collection c, ObjectMatcher f) { for (Iterator it = c.iterator(); it.hasNext();) { Object item = it.next(); if (!f.matches(item)) it.remove(); diff --git a/icu4j/src/com/ibm/icu/impl/PrettyPrinter.java b/icu4j/src/com/ibm/icu/impl/PrettyPrinter.java index c4f9c107765..388cdd9bcfd 100644 --- a/icu4j/src/com/ibm/icu/impl/PrettyPrinter.java +++ b/icu4j/src/com/ibm/icu/impl/PrettyPrinter.java @@ -112,6 +112,7 @@ public class PrettyPrinter { * @return formatted UnicodeSet */ public String toPattern(UnicodeSet uset) { + first = true; // make sure that comparison separates all strings, even canonically equivalent ones Set orderedStrings = new TreeSet(ordering); for (UnicodeSetIterator it = new UnicodeSetIterator(uset); it.next();) { diff --git a/icu4j/src/com/ibm/icu/text/UnicodeSet.java b/icu4j/src/com/ibm/icu/text/UnicodeSet.java index 6656bf09667..0d7703ececa 100755 --- a/icu4j/src/com/ibm/icu/text/UnicodeSet.java +++ b/icu4j/src/com/ibm/icu/text/UnicodeSet.java @@ -3447,7 +3447,7 @@ public class UnicodeSet extends UnicodeFilter { * @deprecated * @author medavis */ - abstract static class XSymbolTable implements SymbolTable { + abstract public static class XSymbolTable implements SymbolTable { public UnicodeMatcher lookupMatcher(int i) { return null; } diff --git a/icu4j/src/com/ibm/icu/util/Freezable.java b/icu4j/src/com/ibm/icu/util/Freezable.java index 63f8a66dfd7..e4f2680f1b7 100644 --- a/icu4j/src/com/ibm/icu/util/Freezable.java +++ b/icu4j/src/com/ibm/icu/util/Freezable.java @@ -7,15 +7,304 @@ package com.ibm.icu.util; /** - * Provides a flexible mechanism for controlling access, without requiring that a class be immutable. - * Once locked, an object can never be unlocked, so it is thread-safe from that point onward. - * The implementation of both methods must be synchronized. - * Once the object has been locked, it must guarantee that no changes can be made to it. - * Any attempt to alter it must raise an UnsupportedOperationException exception. - * This means that when the object returns internal objects, - * or if anyone has references to those internal objects, that those internal objects must either be immutable, - * or must also raise exceptions if any attempt to modify them is made. Of course, the object can return clones - * of internal objects, since those are safe. * @author davis + *
+ *  DRAFT
+ *  Copyright (C) 2005, International Business Machines Corporation and
+ *  others. All Rights Reserved.
+ * 
+ * + * Provides a flexible mechanism for controlling access, without requiring that + * a class be immutable. Once locked, an object can never be unlocked, so it is + * thread-safe from that point onward. The implementation of both methods must + * be synchronized. Once the object has been locked, it must guarantee that no + * changes can be made to it. Any attempt to alter it must raise an + * UnsupportedOperationException exception. This means that when the object + * returns internal objects, or if anyone has references to those internal + * objects, that those internal objects must either be immutable, or must also + * raise exceptions if any attempt to modify them is made. Of course, the object + * can return clones of internal objects, since those are safe. + *

Background

+ *

+ * There are often times when you need objects to be objects 'safe', so that + * they can't be modified. Examples are when objects need to be thread-safe, or + * in writing robust code, or in caches. If you are only creating your own + * objects, you can guarantee this, of course -- but only if you don't make a + * mistake. If you have objects handed into you, or are creating objects using + * others handed into you, it is a different story. It all comes down to whether + * you want to take the Blanche Dubois approach ("depend on the kindness of + * strangers") or the Andy Grove approach ("Only the Paranoid + * Survive"). + *

+ *

+ * For example, suppose we have a simple class: + *

+ * + *
+ * public class A {
+ * 	protected Collection b;
+ * 
+ * 	protected Collection c;
+ * 
+ * 	public Collection get_b() {
+ * 		return b;
+ * 	}
+ * 
+ * 	public Collection get_c() {
+ * 		return c;
+ * 	}
+ * 
+ * 	public A(Collection new_b, Collection new_c) {
+ * 		b = new_b;
+ * 		c = new_c;
+ * 	}
+ * }
+ * 
+ * + *

+ * Since the class doesn't have any setters, someone might think that it is + * immutable. You know where this is leading, of course; this class is unsafe in + * a number of ways. The following illustrates that. + *

+ * + *
+ *  public test1(SupposedlyImmutableClass x, SafeStorage y) {
+ *   <font color="#0000FF">    <b>// unsafe getter</b>
+ *   </font>    A a = x.getA();
+ *   Collection col = a.get_b();
+ *   col.add(something);<font color="#0000FF"> // a has now been changed, and x too
+ *   </font>
+ *   <font color="#0000FF"><b>// unsafe constructor</b></font>
+ *   a = new A(col, col);
+ *   y.store(a);
+ *   col.add(something);<font color="#0000FF"> // a has now been changed, and y too
+ *  
+ *   </font>}
+ * 
+ * + *

+ * There are a few different techniques for having safe classes. + *

+ *
    + *
  1. Const objects. In C++, you can declare parameters const.
  2. + *
  3. Immutable wrappers. For example, you can put a collection in an + * immutable wrapper.
  4. + *
  5. Always-Immutable objects. Java uses this approach, with a few + * variations. Examples: + *
      + *
    1. Simple. Once a Color is created (eg from R, G, and B integers) it is + * immutable.
    2. + *
    3. Builder Class. There is a separate 'builder' class. For example, + * modifiable Strings are created using StringBuffer (which doesn't have the + * full String API available). Once you want an immutable form, you create one + * with toString().
    4. + *
    5. Primitives. These are always safe, since they are copied on input/output + * from methods.
    6. + *
    + *
  6. + *
  7. Cloning. Where you need an object to be safe, you clone it.
  8. + *
+ *

+ * There are advantages and disadvantages of each of these. + *

+ *
    + *
  1. Const provides a certain level of protection, but since const can be and + * is often cast away, it only protects against most inadvertent mistakes. It + * also offers no threading protection, since anyone who has a pointer to the + * (unconst) object in another thread can mess you up.
  2. + *
  3. Immutable wrappers are safer than const in that the constness can't be + * cast away. But other than that they have all the same problems: not safe if + * someone else keeps hold of the original object, or if any of the objects + * returned by the class are mutable.
  4. + *
  5. Always-Immutable Objects are safe, but usage can require excessive + * object creation.
  6. + *
  7. Cloning is only safe if the object truly has a 'safe' clone; defined as + * one that ensures that no change to the clone affects the original. + * Unfortunately, many objects don't have a 'safe' clone, and always cloning can + * require excessive object creation.
  8. + *
+ *

Freezable Model

+ *

+ * The Freezable model supplements these choices by giving you + * the ability to build up an object by calling various methods, then when it is + * in a final state, you can make it immutable. Once immutable, an + * object cannot ever be modified, and is completely thread-safe: that + * is, multiple threads can have references to it without any synchronization. + * If someone needs a mutable version of an object, they can use + * cloneAsThawed(), and modify the copy. This provides a simple, + * effective mechanism for safe classes in circumstances where the alternatives + * are insufficient or clumsy. (If an object is shared before it is immutable, + * then it is the responsibility of each thread to mutex its usage (as with + * other objects).) + *

+ *

+ * Here is what needs to be done to implement this interface, depending on the + * type of the object. + *

+ *

Immutable Objects

+ *

+ * These are the easiest. You just use the interface to reflect that, by adding + * the following: + *

+ * + *
+ *  public class A implements Freezable {
+ *   ...
+ *   public final boolean isFrozen() {return true;}
+ *   public final Object freeze() {return this;}
+ *   public final Object cloneAsThawed() { return this; }
+ *   }
+ * 
+ * + *

+ * These can be final methods because subclasses of immutable objects must + * themselves be immutable. (Note: freeze is returning + * this for chaining.) + *

+ *

Mutable Objects

+ *

+ * Add a protected 'flagging' field: + *

+ * + *
+ * protected boolean immutable;
+ * 
+ * + *

+ * Add the following methods: + *

+ * + *
+ * public final boolean isFrozen() {
+ * 	return frozen;
+ * };
+ * 
+ * public Object freeze() {
+ * 	frozen = true;
+ * 	return this;
+ * }
+ * 
+ * + *

+ * Add a cloneAsThawed() method following the normal pattern for + * clone(), except that frozen=false in the new + * clone. + *

+ *

+ * Then take the setters (that is, any method that can change the internal state + * of the object), and add the following as the first statement: + *

+ * + *
+ * if (isFrozen()) {
+ * 	throw new UnsupportedOperationException("Attempt to modify frozen object");
+ * }
+ * 
+ * + *

Subclassing

+ *

+ * Any subclass of a Freezable will just use its superclass's + * flagging field. It must override freeze() and + * cloneAsThawed() to call the superclass, but normally does not + * override isFrozen(). It must then just pay attention to its + * own getters, setters and fields. + *

+ *

Internal Caches

+ *

+ * Internal caches are cases where the object is logically unmodified, but + * internal state of the object changes. For example, there are const C++ + * functions that cast away the const on the "this" pointer in order + * to modify an object cache. These cases are handled by mutexing the internal + * cache to ensure thread-safety. For example, suppose that UnicodeSet had an + * internal marker to the last code point accessed. In this case, the field is + * not externally visible, so the only thing you need to do is to synchronize + * the field for thread safety. + *

+ *

Unsafe Internal Access

+ *

+ * Internal fields are called safe if they are either + * frozen or immutable (such as String or primitives). If you've + * never allowed internal access to these, then you are all done. For example, + * converting UnicodeSet to be Freezable is just accomplished + * with the above steps. But remember that you have allowed + * access to unsafe internals if you have any code like the following, in a + * getter, setter, or constructor: + *

+ * + *
+ * Collection getStuff() {
+ * 	return stuff;
+ * } // caller could keep reference & modify
+ * 
+ * void setStuff(Collection x) {
+ * 	stuff = x;
+ * } // caller could keep reference & modify
+ * 
+ * MyClass(Collection x) {
+ * 	stuff = x;
+ * } // caller could keep reference & modify
+ * 
+ * + *

+ * These also illustrated in the code sample in Background above. + *

+ *

+ * To deal with unsafe internals, the simplest course of action is to do the + * work in the + freeze() function. Just make all of your internal + * fields frozen, and set the frozen flag. Any subsequent getter/setter will + * work properly. Here is an example: + *

+ * + *
+ * public Object freeze() {
+ * 	if (!frozen) {
+ * 		foo.freeze();
+ * 		frozen = true;
+ * 	}
+ * 	return this;
+ * }
+ * 
+ * + *

+ * If the field is a Collection or Map, then to + * make it frozen you have two choices. If you have never allowed access to the + * collection from outside your object, then just wrap it to prevent future + * modification. + *

+ * + *
+ * zone_to_country = Collections.unmodifiableMap(zone_to_country);
+ * 
+ * + *

+ * If you have ever allowed access, then do a clone() + * before wrapping it. + *

+ * + *
+ * zone_to_country = Collections.unmodifiableMap(zone_to_country.clone());
+ * 
+ * + *

+ * If a collection (or any other container of objects) itself can + * contain mutable objects, then for a safe clone you need to recurse through it + * to make the entire collection immutable. The recursing code should pick the + * most specific collection available, to avoid the necessity of later + * downcasing. + *

+ *
+ *

+ * Note: An annoying flaw in Java is that the generic collections, like + * Map or Set, don't have a clone() + * operation. When you don't know the type of the collection, the simplest + * course is to just create a new collection: + *

+ * + *
+ * zone_to_country = Collections.unmodifiableMap(new HashMap(zone_to_country));
+ * 
+ * + *
*/ public interface Freezable extends Cloneable { /** diff --git a/icu4j/src/com/ibm/icu/util/GlobalizationPreferences.java b/icu4j/src/com/ibm/icu/util/GlobalizationPreferences.java index 5e6b574bf3c..0ad83161cce 100644 --- a/icu4j/src/com/ibm/icu/util/GlobalizationPreferences.java +++ b/icu4j/src/com/ibm/icu/util/GlobalizationPreferences.java @@ -23,11 +23,23 @@ import java.util.regex.Pattern; import com.ibm.icu.impl.Utility; import com.ibm.icu.impl.ZoneMeta; +import com.ibm.icu.text.Collator; import com.ibm.icu.text.DateFormat; +import com.ibm.icu.text.DecimalFormat; +import com.ibm.icu.text.DecimalFormatSymbols; import com.ibm.icu.text.NumberFormat; import com.ibm.icu.text.SimpleDateFormat; - /** + * Copyright (C) 2004-2005, International Business Machines Corporation and * + * others. All Rights Reserved. + */ +/** + *
+ *  DRAFT
+ *  Copyright (C) 2005, International Business Machines Corporation and
+ *  others. All Rights Reserved.
+ * 
+ * * This convenience class provides a mechanism for bundling together different * globalization preferences. It includes: * + * The class will heuristically compute implicit, heuristic values for the above + * based on available data if explicit values are not supplied. These implicit + * values can be presented to users for confirmation, or replacement if the + * values are incorrect. + *

+ * To reset any explicit field so that it will get heuristic values, pass in + * null. For example, myPreferences.setLocale(null); + *

+ * All of the heuristics can be customized by subclasses, by overriding + * getTerritory(), guessCollator(), etc. + *

+ * The class also supplies display names for languages, scripts, territories, + * currencies, timezones, etc. These are computed according to the + * locale/language preference list. Thus, if the preference is Breton; French; + * English, then the display name for a language will be returned in Breton if + * available, otherwise in French if available, otherwise in English. + *

+ * The codes used to reference territory, currency, etc. are as defined elsewhere in ICU, + * and are taken from CLDR (which reflects RFC 3066bis usage, ISO 4217, and the + * TZ Timezone database identifiers). + *

+ * This is at a prototype stage, and has not incorporated all the design + * changes that we would like yet; further feedback is welcome.

+ *

+ * TODO:

+ * Note: + * +======= *
  • And explicit overrides for date/time formats, etc.
  • * The class will heuristically compute implicit, heuristic values for the above based on available * data if explicit values are not supplied. These implicit values can be presented to users @@ -50,8 +113,9 @@ import com.ibm.icu.text.SimpleDateFormat; * * @internal * @deprecated ICU 3.4.2 +>>>>>>> 1.8 */ -public class GlobalizationPreferences { +public class GlobalizationPreferences implements Freezable { /** * Number Format types */ @@ -75,57 +139,85 @@ public class GlobalizationPreferences { * @param locales list of locales in priority order, eg {"be", "fr"} for Breton first, then French if that fails. * @return this, for chaining */ - public GlobalizationPreferences setULocales(List locales) { - this.locales = new ArrayList(locales); - explicitLocales = true; - if (!explicitTerritory) guessTerritory(); - if (!explicitCurrency) guessCurrency(); - if (!explicitTimezone) guessTimeZone(); - if (!explicitCalendar) guessCalendar(); + public GlobalizationPreferences setLocales(List locales) { + if (isFrozen()) { + throw new UnsupportedOperationException("Attempt to modify immutable object"); + } + if (locales.size() == 0) { + this.locales = locales.get(0); + } else { + this.locales = new ArrayList(locales); // clone for safety + } return this; } /** + * Get a copy of the language/locale priority list * @return a copy of the language/locale priority list. */ - public List getULocales() { - return new ArrayList(locales); // clone for safety + public List getLocales() { + List result = new ArrayList(); // clone for safety + if (locales == null) { + result = guessLocales(); + } else if (locales instanceof ULocale) { + result.add(locales); + } else { + result.addAll((List)locales); + } + return result; } /** * Convenience function for getting the locales in priority order - * @return first item. + * @param index The index (0..n) of the desired item. + * @return desired item. */ - public ULocale getULocale(int i) { - return (ULocale)locales.get(i); + public ULocale getLocale(int index) { + if (locales == null) { + return (ULocale)guessLocales().get(index); + } else if (locales instanceof ULocale) { + if (index != 0) throw new IllegalArgumentException("Out of bounds: " + index); + return (ULocale)locales; + } else { + return (ULocale)((List)locales).get(index); + } } /** * Convenience routine for setting the language/locale priority list from an array. - * @see #setULocales(List locales) + * @see #setLocales(List locales) * @param uLocales list of locales in an array * @return this, for chaining */ - public GlobalizationPreferences setULocales(ULocale[] uLocales) { - return setULocales(Arrays.asList(uLocales)); + public GlobalizationPreferences setLocales(ULocale[] uLocales) { + if (isFrozen()) { + throw new UnsupportedOperationException("Attempt to modify immutable object"); + } + return setLocales(Arrays.asList(uLocales)); } /** * Convenience routine for setting the language/locale priority list from a single locale/language. - * @see #setULocales(List locales) + * @see #setLocales(List locales) * @param uLocale single locale * @return this, for chaining */ - public GlobalizationPreferences setULocales(ULocale uLocale) { - return setULocales(new ULocale[]{uLocale}); + public GlobalizationPreferences setLocale(ULocale uLocale) { + if (isFrozen()) { + throw new UnsupportedOperationException("Attempt to modify immutable object"); + } + return setLocales(new ULocale[]{uLocale}); } //#ifndef FOUNDATION /** * Convenience routine for setting the locale priority list from an Accept-Language string. - * @see #setULocales(List locales) + * @see #setLocales(List locales) * @param acceptLanguageString Accept-Language list, as defined by Section 14.4 of the RFC 2616 (HTTP 1.1) * @return this, for chaining */ - public GlobalizationPreferences setULocales(String acceptLanguageString) { + public GlobalizationPreferences setLocales(String acceptLanguageString) { + if (isFrozen()) { + throw new UnsupportedOperationException("Attempt to modify immutable object"); + } /* Accept-Language = "Accept-Language" ":" 1#( language-range [ ";" "q" "=" qvalue ] ) x matches x-... @@ -160,7 +252,7 @@ public class GlobalizationPreferences { result.add(0, new ULocale((String)it2.next())); } } - return setULocales(result); + return setLocales(result); } //#endif @@ -172,11 +264,10 @@ public class GlobalizationPreferences { * @return this, for chaining */ public GlobalizationPreferences setTerritory(String territory) { + if (isFrozen()) { + throw new UnsupportedOperationException("Attempt to modify immutable object"); + } this.territory = territory; - explicitTerritory = true; - if (!explicitCurrency) guessCurrency(); - if (!explicitTimezone) guessTimeZone(); - if (!explicitCalendar) guessCalendar(); return this; } /** @@ -184,6 +275,7 @@ public class GlobalizationPreferences { * @return territory code, explicit or implicit. */ public String getTerritory() { + if (territory == null) return guessTerritory(); return territory; // immutable, so don't need to clone } @@ -193,8 +285,10 @@ public class GlobalizationPreferences { * @return this, for chaining */ public GlobalizationPreferences setCurrency(Currency currency) { + if (isFrozen()) { + throw new UnsupportedOperationException("Attempt to modify immutable object"); + } this.currency = currency; - explicitCurrency = true; return this; } /** @@ -202,6 +296,7 @@ public class GlobalizationPreferences { * @return currency code, explicit or implicit. */ public Currency getCurrency() { + if (currency == null) return guessCurrency(); return currency; // immutable, so don't have to clone } @@ -211,42 +306,79 @@ public class GlobalizationPreferences { * @return this, for chaining */ public GlobalizationPreferences setCalendar(Calendar calendar) { - this.calendar = calendar; - explicitCalendar = true; + if (isFrozen()) { + throw new UnsupportedOperationException("Attempt to modify immutable object"); + } + this.calendar = calendar; return this; } /** * Get a copy of the calendar according to the settings. - * @return currency code, explicit or implicit. + * @return calendar explicit or implicit. */ public Calendar getCalendar() { - return (Calendar) calendar.clone(); // clone for safety + if (calendar == null) return guessCalendar(); + Calendar temp = (Calendar) calendar.clone(); // clone for safety + temp.setTimeZone(getTimeZone()); + return temp; } /** * Sets the timezone ID. If this has not been set, uses default for territory. * @param timezone a valid TZID (see UTS#35). - * @return the object, for chaining. + * @return this, for chaining */ - public GlobalizationPreferences setTimezone(TimeZone timezone) { + public GlobalizationPreferences setTimeZone(TimeZone timezone) { + if (isFrozen()) { + throw new UnsupportedOperationException("Attempt to modify immutable object"); + } this.timezone = timezone; - explicitTimezone = true; return this; } /** * Get the timezone. It was either explicitly set, or is heuristically computed from other settings. * @return timezone, either implicitly or explicitly set */ - public TimeZone getTimezone() { + public TimeZone getTimeZone() { + if (timezone == null) return guessTimeZone(); return (TimeZone) timezone.clone(); // clone for safety } + + /** + * Get a copy of the collator according to the settings. + * @return collator explicit or implicit. + */ + public Collator getCollator() { + if (collator == null) return guessCollator(); + try { + return (Collator) collator.clone(); // clone for safety + } catch (CloneNotSupportedException e) { + throw new InternalError("Error in cloning collator"); + } + } + + /** + * Explicitly set the collator for this object. + * @param collator + * @return this, for chaining + */ + public GlobalizationPreferences setCollator(Collator collator) { + if (isFrozen()) { + throw new UnsupportedOperationException("Attempt to modify immutable object"); + } + this.collator = collator; + return this; + } /** * Set the date locale. * @param dateLocale If not null, overrides the locale priority list for all the date formats. - * @return the object, for chaining + * @return this, for chaining */ public GlobalizationPreferences setDateLocale(ULocale dateLocale) { + if (isFrozen()) { + throw new UnsupportedOperationException("Attempt to modify immutable object"); + } this.dateLocale = dateLocale; return this; } @@ -255,15 +387,18 @@ public class GlobalizationPreferences { * @return date locale. Null if none was set explicitly. */ public ULocale getDateLocale() { - return dateLocale; + return dateLocale != null ? dateLocale : getLocale(0); } /** * Set the number locale. * @param numberLocale If not null, overrides the locale priority list for all the date formats. - * @return the object, for chaining + * @return this, for chaining */ public GlobalizationPreferences setNumberLocale(ULocale numberLocale) { + if (isFrozen()) { + throw new UnsupportedOperationException("Attempt to modify immutable object"); + } this.numberLocale = numberLocale; return this; } @@ -273,7 +408,7 @@ public class GlobalizationPreferences { * @return number locale. Null if none was set explicitly. */ public ULocale getNumberLocale() { - return numberLocale; + return numberLocale != null ? numberLocale : getLocale(0); } /** @@ -285,7 +420,7 @@ public class GlobalizationPreferences { */ public String getDisplayName(String id, int type) { String result = id; - for (Iterator it = locales.iterator(); it.hasNext();) { + for (Iterator it = getLocales().iterator(); it.hasNext();) { ULocale locale = (ULocale) it.next(); switch (type) { case LOCALEID: @@ -328,9 +463,9 @@ public class GlobalizationPreferences { // TODO, have method that doesn't require us to create a timezone // fix other hacks // hack for couldn't match - // note, compiling with FOUNDATION omits this check for now +// note, compiling with FOUNDATION omits this check for now //#ifndef FOUNDATION - if (badTimezone.reset(result).matches()) continue; + if (badTimeZone.reset(result).matches()) continue; //#endif break; default: @@ -344,51 +479,80 @@ public class GlobalizationPreferences { } //#ifndef FOUNDATION // TODO remove need for this - private static final Matcher badTimezone = Pattern.compile("[A-Z]{2}|.*\\s\\([A-Z]{2}\\)").matcher(""); + private static final Matcher badTimeZone = Pattern.compile("[A-Z]{2}|.*\\s\\([A-Z]{2}\\)").matcher(""); //#endif + /** * Set an explicit date format. Overrides both the date locale, and the locale priority list * for a particular combination of dateStyle and timeStyle. NONE should be used if for the style, * where only the date or time format individually is being set. - * @param dateStyle - * @param timeStyle + * @param dateStyle NONE, or DateFormat.FULL, LONG, MEDIUM, SHORT + * @param timeStyle NONE, or DateFormat.FULL, LONG, MEDIUM, SHORT * @param format * @return this, for chaining */ public GlobalizationPreferences setDateFormat(int dateStyle, int timeStyle, DateFormat format) { - if (dateFormats == null) dateFormats = new DateFormat[NONE+1][NONE+1]; + if (isFrozen()) { + throw new UnsupportedOperationException("Attempt to modify immutable object"); + } + if (dateFormats == null) dateFormats = new Object[NONE+1][NONE+1]; dateFormats[dateStyle][timeStyle] = (DateFormat) format.clone(); // for safety return this; } + /** + * Set an explicit date format. Overrides both the date locale, and the locale priority list + * for a particular combination of dateStyle and timeStyle. NONE should be used if for the style, + * where only the date or time format individually is being set. + * @param dateStyle NONE, or DateFormat.FULL, LONG, MEDIUM, SHORT + * @param timeStyle NONE, or DateFormat.FULL, LONG, MEDIUM, SHORT + * @param formatPattern date pattern, eg "yyyy-MMM-dd" + * @return this, for chaining + */ + public GlobalizationPreferences setDateFormat(int dateStyle, int timeStyle, String formatPattern) { + if (isFrozen()) { + throw new UnsupportedOperationException("Attempt to modify immutable object"); + } + if (dateFormats == null) dateFormats = new Object[NONE+1][NONE+1]; + // test the format to make sure it won't throw an error later + new SimpleDateFormat(formatPattern, getDateLocale()); + dateFormats[dateStyle][timeStyle] = formatPattern; // for safety + return this; + } /** * Gets a date format according to the current settings. If there is an explicit (non-null) date/time * format set, a copy of that is returned. Otherwise, if there is a non-null date locale, that is used. * Otherwise, the language priority list is used. NONE should be used for the style, * where only the date or time format individually is being gotten. - * @param dateStyle - * @param timeStyle + * @param dateStyle NONE, or DateFormat.FULL, LONG, MEDIUM, SHORT + * @param timeStyle NONE, or DateFormat.FULL, LONG, MEDIUM, SHORT * @return a DateFormat, according to the above description */ public DateFormat getDateFormat(int dateStyle, int timeStyle) { try { DateFormat result = null; - if (dateFormats != null) result = dateFormats[dateStyle][timeStyle]; + if (dateFormats != null) { // and override can either be a string or a pattern + Object temp = dateFormats[dateStyle][timeStyle]; + if (temp instanceof DateFormat) { + result = (DateFormat) temp; + } else { + result = new SimpleDateFormat((String)temp, getDateLocale()); + } + } if (result != null) { result = (DateFormat) result.clone(); // clone for safety - result.setCalendar(calendar); + result.setCalendar(getCalendar()); } else { // In the case of date formats, we don't have to look at more than one // locale. May be different for other cases - ULocale currentLocale = dateLocale != null ? dateLocale : (ULocale)locales.get(0); // TODO Make this one function. if (timeStyle == NONE) { - result = DateFormat.getDateInstance(calendar, dateStyle, currentLocale); + result = DateFormat.getDateInstance(getCalendar(), dateStyle, getDateLocale()); } else if (dateStyle == NONE) { - result = DateFormat.getTimeInstance(calendar, timeStyle, currentLocale); + result = DateFormat.getTimeInstance(getCalendar(), timeStyle, getDateLocale()); } else { - result = DateFormat.getDateTimeInstance(calendar, dateStyle, timeStyle, currentLocale); + result = DateFormat.getDateTimeInstance(getCalendar(), dateStyle, timeStyle, getDateLocale()); } } return result; @@ -404,32 +568,39 @@ public class GlobalizationPreferences { /** * Gets a number format according to the current settings. * If there is an explicit (non-null) number - * format set, a copy of that is returned. Otherwise, if there is a non-null date locale, that is used. + * format set, a copy of that is returned. Otherwise, if there is a non-null number locale, that is used. * Otherwise, the language priority list is used. NONE should be used for the style, * where only the date or time format individually is being gotten. + * @param style CURRENCY, NUMBER, INTEGER, SCIENTIFIC, PERCENT */ public NumberFormat getNumberFormat(int style) { try { NumberFormat result = null; - if (numberFormats != null) result = numberFormats[style]; + if (numberFormats != null) { + Object temp = numberFormats[style]; + if (temp instanceof NumberFormat) { + result = (NumberFormat) temp; + } else { + result = new DecimalFormat((String)temp, new DecimalFormatSymbols(getDateLocale())); + } + } if (result != null) { - result = (NumberFormat) result.clone(); // clone for safety + result = (NumberFormat) result.clone(); // clone for safety (later optimize) if (style == CURRENCY) { - result.setCurrency(currency); + result.setCurrency(getCurrency()); } return result; } // In the case of date formats, we don't have to look at more than one // locale. May be different for other cases - ULocale currentLocale = numberLocale != null ? numberLocale : (ULocale)locales.get(0); switch (style) { - case NUMBER: return NumberFormat.getInstance(currentLocale); - case SCIENTIFIC: return NumberFormat.getScientificInstance(currentLocale); - case INTEGER: return NumberFormat.getIntegerInstance(currentLocale); - case PERCENT: return NumberFormat.getPercentInstance(currentLocale); - case CURRENCY: result = NumberFormat.getCurrencyInstance(currentLocale); - result.setCurrency(currency); - return result; + case NUMBER: return NumberFormat.getInstance(getNumberLocale()); + case SCIENTIFIC: return NumberFormat.getScientificInstance(getNumberLocale()); + case INTEGER: return NumberFormat.getIntegerInstance(getNumberLocale()); + case PERCENT: return NumberFormat.getPercentInstance(getNumberLocale()); + case CURRENCY: result = NumberFormat.getCurrencyInstance(getNumberLocale()); + result.setCurrency(getCurrency()); + return result; } } catch (RuntimeException e) {} throw new IllegalArgumentException(); // fix later @@ -437,55 +608,111 @@ public class GlobalizationPreferences { /** * Sets a number format explicitly. Overrides the number locale and the general locale settings. + * @param style CURRENCY, NUMBER, INTEGER, SCIENTIFIC, PERCENT + * @return this, for chaining */ public GlobalizationPreferences setNumberFormat(int style, DateFormat format) { - if (numberFormats == null) numberFormats = new NumberFormat[NUMBER_LIMIT]; + if (isFrozen()) { + throw new UnsupportedOperationException("Attempt to modify immutable object"); + } + if (numberFormats == null) numberFormats = new Object[NUMBER_LIMIT]; numberFormats[style] = (NumberFormat) format.clone(); // for safety return this; } /** - * Restore the object to the initial state. - * @return the object, for chaining + * Sets a number format explicitly. Overrides the number locale and the general locale settings. + * @return this, for chaining */ - public GlobalizationPreferences clear() { - explicitLocales = explicitTerritory = explicitCurrency = explicitTimezone = explicitCalendar = false; - locales.add(ULocale.getDefault()); - if (!explicitTerritory) guessTerritory(); - if (!explicitCurrency) guessCurrency(); - if (!explicitTimezone) guessTimeZone(); - if (!explicitCalendar) guessCalendar(); + public GlobalizationPreferences setNumberFormat(int style, String formatPattern) { + if (isFrozen()) { + throw new UnsupportedOperationException("Attempt to modify immutable object"); + } + if (numberFormats == null) numberFormats = new Object[NUMBER_LIMIT]; + // check to make sure it compiles + new DecimalFormat((String)formatPattern, new DecimalFormatSymbols(getDateLocale())); + numberFormats[style] = formatPattern; // for safety + return this; + } + + /** + * Restore the object to the initial state. + * @return this, for chaining + */ + public GlobalizationPreferences reset() { + if (isFrozen()) { + throw new UnsupportedOperationException("Attempt to modify immutable object"); + } + territory = null; + calendar = null; + collator = null; + timezone = null; + currency = null; + dateFormats = null; + numberFormats = null; + dateLocale = null; + numberLocale = null; + locales = null; return this; } - // protected helper functions - protected void guessTerritory() { + /** + * This function can be overridden by subclasses to use different heuristics. + */ + protected String guessTerritory() { + String result; // pass through locales to see if there is a territory. - for (Iterator it = locales.iterator(); it.hasNext();) { + for (Iterator it = getLocales().iterator(); it.hasNext();) { ULocale locale = (ULocale)it.next(); - String temp = locale.getCountry(); - if (temp.length() != 0) { - territory = temp; - return; + result = locale.getCountry(); + if (result.length() != 0) { + return result; } } // if not, guess from the first language tag, or maybe from intersection of languages, eg nl + fr => BE // TODO fix using real data // for now, just use fixed values - ULocale firstLocale = (ULocale)locales.iterator().next(); + ULocale firstLocale = getLocale(0); String language = firstLocale.getLanguage(); String script = firstLocale.getScript(); - territory = null; + result = null; if (script.length() != 0) { - territory = (String) language_territory_hack_map.get(language + "_" + script); + result = (String) language_territory_hack_map.get(language + "_" + script); } - if (territory == null) territory = (String) language_territory_hack_map.get(language); - if (territory == null) territory = "US"; // need *some* default + if (result == null) result = (String) language_territory_hack_map.get(language); + if (result == null) result = "US"; // need *some* default + return result; } - protected void guessCurrency() { - currency = Currency.getInstance(new ULocale("und-" + territory)); + /** + * This function can be overridden by subclasses to use different heuristics + */ + protected Currency guessCurrency() { + return Currency.getInstance(new ULocale("und-" + getTerritory())); } - protected void guessTimeZone() { + /** + * This function can be overridden by subclasses to use different heuristics + * It MUST return a 'safe' value, + * one whose modification will not affect this object. + */ + protected List guessLocales() { + List result = new ArrayList(0); + result.add(ULocale.getDefault()); + return result; + } + /** + * This function can be overridden by subclasses to use different heuristics. + * It MUST return a 'safe' value, + * one whose modification will not affect this object. + */ + protected Collator guessCollator() { + return Collator.getInstance(getLocale(0)); + } + /** + * This function can be overridden by subclasses to use different heuristics. + * It MUST return a 'safe' value, + * one whose modification will not affect this object. + */ + protected TimeZone guessTimeZone() { // TODO fix using real data // for single-zone countries, pick that zone // for others, pick the most populous zone @@ -493,9 +720,9 @@ public class GlobalizationPreferences { // NOTE: in a few cases can do better by looking at language. // Eg haw+US should go to Pacific/Honolulu // fr+CA should go to America/Montreal - String timezoneString = (String) territory_tzid_hack_map.get(territory); + String timezoneString = (String) territory_tzid_hack_map.get(getTerritory()); if (timezoneString == null) { - String[] attempt = ZoneMeta.getAvailableIDs(territory); + String[] attempt = ZoneMeta.getAvailableIDs(getTerritory()); if (attempt.length == 0) { timezoneString = "Etc/GMT"; // gotta do something } else { @@ -508,35 +735,39 @@ public class GlobalizationPreferences { timezoneString = attempt[i]; } } - timezone = TimeZone.getTimeZone(timezoneString); + return TimeZone.getTimeZone(timezoneString); } - protected void guessCalendar() { + /** + * This function can be overridden by subclasses to use different heuristics. + * It MUST return a 'safe' value, + * one whose modification will not affect this object. + */ + protected Calendar guessCalendar() { // TODO add better API - calendar = Calendar.getInstance(new ULocale("und-" + territory)); + return Calendar.getInstance(new ULocale("und-" + getTerritory())); } // PRIVATES - private ArrayList locales = new ArrayList(); + private Object locales; private String territory; private Currency currency; private TimeZone timezone; private Calendar calendar; - private boolean explicitLocales; - private boolean explicitTerritory; - private boolean explicitCurrency; - private boolean explicitTimezone; - private boolean explicitCalendar; + private Collator collator; private ULocale dateLocale; - private DateFormat[][] dateFormats; + private Object[][] dateFormats; private ULocale numberLocale; - private NumberFormat[] numberFormats; + private Object[] numberFormats; { - clear(); + reset(); } - // + + /** WARNING: All of this data is temporary, until we start importing from CLDR!!! + * + */ private static final Map language_territory_hack_map = new HashMap(); private static final String[][] language_territory_hack = { {"af", "ZA"}, @@ -737,4 +968,24 @@ public class GlobalizationPreferences { territory_tzid_hack_map.put(territory_tzid_hack[i][0],territory_tzid_hack[i][1]); } } + + protected boolean frozen; + public boolean isFrozen() { + return frozen; + } + public Object freeze() { + frozen = true; + return this; + } + public Object cloneAsThawed() { + try { + GlobalizationPreferences result = (GlobalizationPreferences) clone(); + result.frozen = false; + return result; + } catch (CloneNotSupportedException e) { + // will always work + return null; + } + } } +