diff --git a/icu4j/src/com/ibm/icu/dev/test/util/ICUServiceTest.java b/icu4j/src/com/ibm/icu/dev/test/util/ICUServiceTest.java new file mode 100644 index 00000000000..febbf53c400 --- /dev/null +++ b/icu4j/src/com/ibm/icu/dev/test/util/ICUServiceTest.java @@ -0,0 +1,571 @@ +package com.ibm.icu.dev.test.util; + +import com.ibm.icu.dev.test.TestFmwk; +import com.ibm.icu.impl.ICUService; +import com.ibm.icu.impl.ICUService.Factory; +import com.ibm.icu.impl.ICUService.Key; +import com.ibm.icu.impl.ICUService.ServiceListener; +import com.ibm.icu.impl.LocaleUtility; +import com.ibm.icu.impl.ICULocaleData; +import com.ibm.icu.impl.ICULocaleService; +import com.ibm.icu.impl.ICULocaleService.LocaleKey; +import com.ibm.icu.impl.ICULocaleService.MultipleKeyFactory; +import com.ibm.icu.impl.ICULocaleService.ICUResourceBundleFactory; +import java.util.Arrays; +import java.util.EventListener; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.TreeSet; + +public class ICUServiceTest extends TestFmwk +{ + public static void main(String[] args) throws Exception { + ICUServiceTest test = new ICUServiceTest(); + test.run(args); + } + + private String lrmsg(String message, Object lhs, Object rhs) { + return message + " lhs: " + lhs + " rhs: " + rhs; + } + + public void confirmBoolean(String message, boolean val) { + logln(message, val, !val); + } + + public void confirmEqual(String message, Object lhs, Object rhs) { + logln(lrmsg(message, lhs, rhs), lhs == null ? rhs == null : lhs.equals(rhs)); + } + + public void confirmIdentical(String message, Object lhs, Object rhs) { + logln(lrmsg(message, lhs, rhs), lhs == rhs); + } + + public void confirmIdentical(String message, int lhs, int rhs) { + logln(message + " lhs: " + lhs + " rhs: " + rhs, lhs == rhs); + } + + // use locale keys + static final class TestService extends ICUService { + protected Key createKey(String id) { + return LocaleKey.createWithCanonicalFallback(id, null); // no fallback locale + } + } + + public void testAPI() { + // create a service using locale keys, + ICUService service = new TestService(); + + // register an object with one locale, + // search for an object with a more specific locale + // should return the original object + Integer singleton0 = new Integer(0); + service.registerObject(singleton0, "en_US"); + Object result = service.get("en_US_FOO"); + confirmIdentical("1) en_US_FOO -> en_US", result, singleton0); + + // register a new object with the more specific locale + // search for an object with that locale + // should return the new object + Integer singleton1 = new Integer(1); + service.registerObject(singleton1, "en_US_FOO"); + result = service.get("en_US_FOO"); + confirmIdentical("2) en_US_FOO -> en_US_FOO", result, singleton1); + + // search for an object that falls back to the first registered locale + result = service.get("en_US_BAR"); + confirmIdentical("3) en_US_BAR -> en_US", result, singleton0); + + // get a list of the factories, should be two + List factories = service.factories(); + confirmIdentical("4) factory size", factories.size(), 2); + + // register a new object with yet another locale + // original factory list is unchanged + Integer singleton2 = new Integer(2); + service.registerObject(singleton2, "en"); + confirmIdentical("5) factory size", factories.size(), 2); + + // search for an object with the new locale + // stack of factories is now en, en_US_FOO, en_US + // search for en_US should still find en_US object + result = service.get("en_US_BAR"); + confirmIdentical("6) en_US_BAR -> en_US", result, singleton0); + + // register a new object with an old id, should hide earlier factory using this id, but leave it there + Integer singleton3 = new Integer(3); + service.registerObject(singleton3, "en_US"); + factories = service.factories(); + confirmIdentical("9) factory size", factories.size(), 4); + + // should get data from that new factory + result = service.get("en_US_BAR"); + confirmIdentical("10) en_US_BAR -> (3)", result, singleton3); + + // remove new factory + // should have fewer factories again + service.unregisterFactory((Factory)factories.get(0)); + factories = service.factories(); + confirmIdentical("11) factory size", factories.size(), 3); + + // should get original data again after remove factory + result = service.get("en_US_BAR"); + confirmIdentical("12) en_US_BAR -> 0", result, singleton0); + + // shouldn't find unregistered ids + result = service.get("foo"); + confirmIdentical("13) foo -> null", result, null); + + // should find non-canonical strings + String[] resultID = new String[1]; + result = service.get("EN_us_fOo", resultID); + confirmEqual("14) find non-canonical", resultID[0], "en_US_FOO"); + + // should be able to register non-canonical strings and get them canonicalized + service.registerObject(singleton3, "eN_ca_dUde"); + result = service.get("En_Ca_DuDe", resultID); + confirmEqual("15) register non-canonical", resultID[0], "en_CA_DUDE"); + + // should be able to register invisible factories, these will not + // be visible by default, but if you know the secret password you + // can still access these services... + Integer singleton4 = new Integer(4); + service.registerObject(singleton4, "en_US_BAR", false); + result = service.get("en_US_BAR"); + confirmIdentical("17) get invisible", result, singleton4); + + // should not be able to locate invisible services + Set ids = service.getVisibleIDs(); + confirmBoolean("18) find invisible", !ids.contains("en_US_BAR")); + + service.reset(); + // an anonymous factory than handles all ids + { + Factory factory = new Factory() { + public Object create(Key key) { + return LocaleUtility.getLocaleFromName(key.currentID()); + } + + public void updateVisibleIDs(Map result) { + } + + public String getDisplayName(String id, Locale l) { + return null; + } + }; + service.registerFactory(factory); + + // anonymous factory will still handle the id + result = service.get(Locale.US.toString()); + confirmEqual("21) locale", result, Locale.US); + + // still normalizes id + result = service.get("EN_US_BAR"); + confirmEqual("22) locale", result, LocaleUtility.getLocaleFromName("en_US_BAR")); + + // we can override for particular ids + service.registerObject(singleton3, "en_US_BAR"); + result = service.get("en_US_BAR"); + confirmIdentical("23) override super", result, singleton3); + } + + // empty service should not recognize anything + service.reset(); + result = service.get("en_US"); + confirmIdentical("24) empty", result, null); + + // create a custom multiple key factory + { + String[] xids = { "en_US_VALLEY_GIRL", + "en_US_VALLEY_BOY", + "en_US_SURFER_GAL", + "en_US_SURFER_DUDE" + }; + service.registerFactory(new TestMultipleFactory(xids)); + } + + // iterate over the visual ids returned by the multiple factory + { + Set vids = service.getVisibleIDs(); + Iterator iter = vids.iterator(); + int count = 0; + while (iter.hasNext()) { + ++count; + logln(" " + iter.next()); + } + // four visible ids + confirmIdentical("25) visible ids", count, 4); + } + + // iterate over the display names + { + Map dids = service.getDisplayNames(Locale.GERMANY); + Iterator iter = dids.entrySet().iterator(); + int count = 0; + while (iter.hasNext()) { + ++count; + Entry e = (Entry)iter.next(); + logln(" " + e.getKey() + " -- > " + e.getValue()); + } + // four display names, in german + confirmIdentical("26) display names", count, 4); + } + + // no valid display name + confirmIdentical("27) get display name", service.getDisplayName("en_US_VALLEY_GEEK"), null); + + { + String name = service.getDisplayName("en_US_SURFER_DUDE", Locale.US); + confirmEqual("28) get display name", name, "English (United States,SURFER,DUDE)"); + } + + // register another multiple factory + { + String[] xids = { + "en_US_SURFER_GAL", "en_US_SILICON", "en_US_SILICON_GEEK", "en_US" + }; + service.registerFactory(new TestMultipleFactory(xids, "Rad dude")); + } + + // this time, we have seven display names (we replaced surfer gal) + { + Map dids = service.getDisplayNames(LocaleUtility.getLocaleFromName("es")); + Iterator iter = dids.entrySet().iterator(); + int count = 0; + while (iter.hasNext()) { + ++count; + Entry e = (Entry)iter.next(); + logln(" " + e.getKey() + " -- > " + e.getValue()); + } + // seven display names, in spanish + confirmIdentical("29) display names", count, 7); + } + + // we should get the display name corresponding to the actual id + // returned by the id we used. + { + String[] actualID = new String[1]; + String id = "en_us_silicon_dude"; + String dude = (String)service.get(id, actualID); + if (dude != null) { + String displayName = service.getDisplayName(actualID[0], Locale.US); + logln("found actual: " + dude + " with display name: " + displayName); + confirmBoolean("30) found display name for actual", displayName != null); + + displayName = service.getDisplayName(id, Locale.US); + logln("found query: " + dude + " with display name: " + displayName); + confirmBoolean("31) found display name for query", displayName == null); + } else { + errln("30) service could not find entry for " + id); + } + + id = "en_US_BOZO"; + String bozo = (String)service.get(id, actualID); + if (bozo != null) { + String displayName = service.getDisplayName(actualID[0], Locale.US); + logln("found actual: " + bozo + " with display name: " + displayName); + confirmBoolean("32) found display name for actual", displayName != null); + + displayName = service.getDisplayName(id, Locale.US); + logln("found actual: " + bozo + " with display name: " + displayName); + confirmBoolean("33) found display name for query", displayName == null); + } else { + errln("32) service could not find entry for " + id); + } + } + + // hiding factory should obscure 'sublocales' + { + String[] xids = { + "en_US_VALLEY", "en_US_SILICON" + }; + service.registerFactory(new TestHidingFactory(xids)); + } + + { + Map dids = service.getDisplayNames(); + Iterator iter = dids.entrySet().iterator(); + int count = 0; + while (iter.hasNext()) { + ++count; + Entry e = (Entry)iter.next(); + logln(" " + e.getKey() + " -- > " + e.getValue()); + } + confirmIdentical("31 hiding factory", count, 5); + } + + { + Set xids = service.getVisibleIDs(); + Iterator iter = xids.iterator(); + while (iter.hasNext()) { + String xid = (String)iter.next(); + logln(xid + "? " + service.get(xid)); + } + + logln("valleygirl? " + service.get("en_US_VALLEY_GIRL")); + logln("valleyboy? " + service.get("en_US_VALLEY_BOY")); + logln("valleydude? " + service.get("en_US_VALLEY_DUDE")); + logln("surfergirl? " + service.get("en_US_SURFER_GIRL")); + } + + // resource bundle factory. + service.reset(); + service.registerFactory(new ICUResourceBundleFactory("Countries;Languages", true)); + + // list all of the resources that really define Countries;Languages + // this takes a long time to build the visible id list + { + Set xids = service.getVisibleIDs(); + StringBuffer buf = new StringBuffer("{"); + boolean notfirst = false; + Iterator iter = xids.iterator(); + while (iter.hasNext()) { + String xid = (String)iter.next(); + if (notfirst) { + buf.append(", "); + } else { + notfirst = true; + } + buf.append(xid); + } + buf.append("}"); + logln(buf.toString()); + } + + // get all the display names of these resources + // this should be fast since the display names were cached. + { + Map names = service.getDisplayNames(LocaleUtility.getLocaleFromName("de_DE")); + StringBuffer buf = new StringBuffer("{"); + Iterator iter = names.entrySet().iterator(); + while (iter.hasNext()) { + Entry e = (Entry)iter.next(); + String name = (String)e.getKey(); + String id = (String)e.getValue(); + buf.append("\n " + name + " --> " + id); + } + buf.append("\n}"); + logln(buf.toString()); + } + + service.registerFactory(new CalifornioLanguageFactory()); + // get all the display names of these resources + { + Map names = service.getDisplayNames(LocaleUtility.getLocaleFromName("en_US_CA_SURFER")); + StringBuffer buf = new StringBuffer("{"); + Iterator iter = names.entrySet().iterator(); + while (iter.hasNext()) { + Entry e = (Entry)iter.next(); + String name = (String)e.getKey(); + String id = (String)e.getValue(); + buf.append("\n " + name + " --> " + id); + } + buf.append("\n}"); + logln(buf.toString()); + } + + // test notification + // simple registration + { + ICULocaleService ls = new ICULocaleService(); + ServiceListener l1 = new ServiceListener() { + private int n; + public void serviceChanged(ICUService s) { + logln("listener 1 report " + n++ + " service changed: " + s); + } + }; + ls.addListener(l1); + ServiceListener l2 = new ServiceListener() { + private int n; + public void serviceChanged(ICUService s) { + logln("listener 2 report " + n++ + " service changed: " + s); + } + }; + ls.addListener(l2); + logln("registering foo... "); + ls.registerObject("Foo", "en_FOO"); + logln("registering bar... "); + ls.registerObject("Bar", "en_BAR"); + logln("getting foo..."); + logln((String)ls.get("en_FOO")); + logln("removing listener 2..."); + ls.removeListener(l2); + logln("registering baz..."); + ls.registerObject("Baz", "en_BAZ"); + logln("removing listener 1"); + ls.removeListener(l1); + logln("registering burp..."); + ls.registerObject("Burp", "en_BURP"); + + // should only get one notification even if register multiple times + logln("... trying multiple registration"); + ls.addListener(l1); + ls.addListener(l1); + ls.addListener(l1); + ls.addListener(l2); + ls.registerObject("Foo", "en_FOO"); + logln("... registered foo"); + + // since in a separate thread, we can callback and not deadlock + ServiceListener l3 = new ServiceListener() { + private int n; + public void serviceChanged(ICUService s) { + logln("listener 3 report " + n++ + " service changed..."); + if (s.get("en_BOINK") == null) { // don't recurse on ourselves!!! + logln("registering boink..."); + s.registerObject("boink", "en_BOINK"); + } + } + }; + ls.addListener(l3); + logln("registering boo..."); + ls.registerObject("Boo", "en_BOO"); + logln("...done"); + + try { + Thread.sleep(100); + } + catch (InterruptedException e) { + } + } + } + + + static class TestMultipleFactory extends MultipleKeyFactory { + protected final String[] ids; + protected final String factoryID; + + public TestMultipleFactory(String[] ids) { + this(ids, ""); + } + + public TestMultipleFactory(String[] ids, String factoryID) { + this.ids = (String[])ids.clone(); + + if (factoryID == null || factoryID.length() == 0) { + this.factoryID = ""; + } else { + this.factoryID = factoryID + ": "; + } + } + + protected Object handleCreate(Key key) { + for (int i = 0; i < ids.length; ++i) { + if (key.currentID().equalsIgnoreCase(ids[i])) { + return factoryID + key.canonicalID(); + } + } + return null; + } + + protected void handleUpdateVisibleIDs(Set result) { + for (int i = 0; i < ids.length; ++i) { + result.add(ids[i]); + } + } + protected String handleGetDisplayName(String id, Locale locale) { + return factoryID + LocaleUtility.getLocaleFromName(id).getDisplayName(locale); + } + } + + static class TestHidingFactory implements ICUService.Factory { + protected final String[] ids; + protected final String factoryID; + + public TestHidingFactory(String[] ids) { + this(ids, "Hiding"); + } + + public TestHidingFactory(String[] ids, String factoryID) { + this.ids = (String[])ids.clone(); + + if (factoryID == null || factoryID.length() == 0) { + this.factoryID = ""; + } else { + this.factoryID = factoryID + ": "; + } + } + + public Object create(Key key) { + for (int i = 0; i < ids.length; ++i) { + if (LocaleUtility.isFallbackOf(ids[i], key.currentID())) { + return factoryID + key.canonicalID(); + } + } + return null; + } + + public void updateVisibleIDs(Map result) { + for (int i = 0; i < ids.length; ++i) { + String id = ids[i]; + Iterator iter = result.keySet().iterator(); + while (iter.hasNext()) { + if (LocaleUtility.isFallbackOf(id, (String)iter.next())) { + iter.remove(); + } + } + result.put(id, this); + } + } + + public String getDisplayName(String id, Locale locale) { + return factoryID + LocaleUtility.getLocaleFromName(id).getDisplayName(locale); + } + } + + static class CalifornioLanguageFactory extends ICUResourceBundleFactory { + CalifornioLanguageFactory() { + super("Countries;Languages", true); + } + + private static String californio = "en_US_CA"; + private static String valley = californio + "_VALLEY"; + private static String surfer = californio + "_SURFER"; + private static String geek = californio + "_GEEK"; + + public void handleUpdateVisibleIDs(Set result) { + super.handleUpdateVisibleIDs(result); + + result.add(californio); + result.add(valley); + result.add(surfer); + result.add(geek); + } + + protected String handleGetDisplayName(String id, Locale locale) { + String prefix = ""; + String suffix = ""; + String ls = locale.toString(); + if (LocaleUtility.isFallbackOf(californio, ls)) { + if (ls.equalsIgnoreCase(valley)) { + prefix = "Like, you know, it's so totally "; + } else if (ls.equalsIgnoreCase(surfer)) { + prefix = "Dude, its "; + } else if (ls.equalsIgnoreCase(geek)) { + prefix = "I'd estimate it's approximately "; + } else { + prefix = "Huh? Maybe "; + } + } + if (LocaleUtility.isFallbackOf(californio, id)) { + if (id.equalsIgnoreCase(valley)) { + suffix = "like the Valley, you know? Let's go to the mall!"; + } else if (id.equalsIgnoreCase(surfer)) { + suffix = "time to hit those gnarly waves, Dude!!!"; + } else if (id.equalsIgnoreCase(geek)) { + suffix = "all systems go. T-Minus 9, 8, 7..."; + } else { + suffix = "No Habla Englais"; + } + } else { + suffix = super.handleGetDisplayName(id, locale); + } + + return prefix + suffix; + } + } +} diff --git a/icu4j/src/com/ibm/icu/dev/test/util/ICUServiceTestSample.java b/icu4j/src/com/ibm/icu/dev/test/util/ICUServiceTestSample.java new file mode 100644 index 00000000000..732d633aa40 --- /dev/null +++ b/icu4j/src/com/ibm/icu/dev/test/util/ICUServiceTestSample.java @@ -0,0 +1,198 @@ +package com.ibm.icu.dev.test.util; + +import com.ibm.icu.impl.ICULocaleService; +import com.ibm.icu.impl.ICUService; +import com.ibm.icu.impl.LocaleUtility; +import java.util.EventListener; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +public class ICUServiceTestSample { + static public void main(String[] args) { + HelloServiceClient client = new HelloServiceClient(); + + Thread t = new HelloUpdateThread(); + t.start(); + try { + t.join(); + } + catch (InterruptedException e) { + } + System.out.println("done"); + } + + /** + * A class that displays the current names in the Hello service. + * Each time the service changes, it redisplays the names. + */ + static class HelloServiceClient implements HelloService.HelloServiceListener { + + HelloServiceClient() { + HelloService.addListener(this); + display(); + } + + /** + * This will be called in the notification thread of + * ICUNotifier. ICUNotifier could spawn a (non-daemon) thread + * for each listener, so that impolite listeners wouldn't hold + * up notification, but right now it doesn't. Instead, all + * notifications are delivered on the notification thread. + * Since that's a daemon thread, a notification might not + * complete before main terminates. + */ + public void helloServiceChanged() { + display(); + } + + private void display() { + Map names = HelloService.getDisplayNames(Locale.US); + System.out.println("displaying " + names.size() + " names."); + Iterator iter = names.entrySet().iterator(); + while (iter.hasNext()) { + Entry entry = (Entry)iter.next(); + String displayName = (String)entry.getKey(); + HelloService service = HelloService.get((String)entry.getValue()); + System.out.println(displayName + " says " + service.hello()); + try { + Thread.sleep(50); + } + catch (InterruptedException e) { + } + } + System.out.println("----"); + } + } + + /** + * A thread to update the service. + */ + static class HelloUpdateThread extends Thread { + String[][] updates = { + { "Hey", "en_US_INFORMAL" }, + { "Hallo", "de_DE_INFORMAL" }, + { "Yo!", "en_US_CALIFORNIA_INFORMAL" }, + { "Chi Fanle Ma?", "zh__INFORMAL" }, + { "Munch munch... Burger?", "en" }, + { "Sniff", "fr" }, + { "TongZhi! MaoZeDong SiXiang Wan Sui!", "zh_CN" }, + { "Bier? Ja!", "de" }, + }; + public void run() { + for (int i = 0; i < updates.length; ++i) { + try { + Thread.sleep(500); + } + catch (InterruptedException e) { + } + HelloService.register(updates[i][0], LocaleUtility.getLocaleFromName(updates[i][1])); + } + } + } + + /** + * An example service that wraps an ICU service in order to export custom API and + * notification. The service just implements 'hello'. + */ + static final class HelloService { + private static ICUService registry; + private String name; + + private HelloService(String name) { + this.name = name; + } + + /** + * The hello service... + */ + public String hello() { + return name; + } + + public String toString() { + return super.toString() + ": " + name; + } + + /** + * Deferred init. + */ + private static ICUService registry() { + if (registry == null) { + initRegistry(); + } + return registry; + } + + private static void initRegistry() { + registry = new ICULocaleService() { + protected boolean acceptsListener(EventListener l) { + return true; // we already verify in our wrapper APIs + } + protected void notifyListener(EventListener l) { + ((HelloServiceListener)l).helloServiceChanged(); + } + }; + + // initialize + doRegister("Hello", "en"); + doRegister("Bonjour", "fr"); + doRegister("Ni Hao", "zh_CN"); + doRegister("Guten Tag", "de"); + } + + /** + * A custom listener for changes to this service. We don't need to + * point to the service since it is defined by this class and not + * an object. + */ + public static interface HelloServiceListener extends EventListener { + public void helloServiceChanged(); + } + + /** + * Type-safe notification for this service. + */ + public static void addListener(HelloServiceListener l) { + registry().addListener(l); + } + + /** + * Type-safe notification for this service. + */ + public static void removeListener(HelloServiceListener l) { + registry().removeListener(l); + } + + /** + * Type-safe access to the service. + */ + public static HelloService get(String id) { + return (HelloService)registry().get(id); + } + + public static Set getVisibleIDs() { + return registry().getVisibleIDs(); + } + + public static Map getDisplayNames(Locale locale) { + return registry().getDisplayNames(locale); + } + + /** + * Register a new hello string for this locale. + */ + public static void register(String helloString, Locale locale) { + if (helloString == null || locale == null) { + throw new NullPointerException(); + } + doRegister(helloString, LocaleUtility.canonicalLocaleString(locale.toString())); + } + + private static void doRegister(String hello, String id) { + registry().registerObject(new HelloService(hello), id); + } + } +} diff --git a/icu4j/src/com/ibm/icu/dev/test/util/ICUServiceThreadTest.java b/icu4j/src/com/ibm/icu/dev/test/util/ICUServiceThreadTest.java new file mode 100644 index 00000000000..974c64bed3b --- /dev/null +++ b/icu4j/src/com/ibm/icu/dev/test/util/ICUServiceThreadTest.java @@ -0,0 +1,443 @@ +package com.ibm.icu.dev.test.util; + +import com.ibm.icu.dev.test.TestFmwk; +import com.ibm.icu.dev.test.TestLog; +import com.ibm.icu.impl.ICUService; +import com.ibm.icu.impl.ICUService.Factory; +import com.ibm.icu.impl.ICUService.SimpleFactory; +import com.ibm.icu.impl.ICUService.Key; +import com.ibm.icu.impl.LocaleUtility; +import com.ibm.icu.impl.ICULocaleData; +import com.ibm.icu.impl.ICULocaleService; +import com.ibm.icu.impl.ICULocaleService.LocaleKey; +import com.ibm.icu.impl.ICULocaleService.MultipleKeyFactory; +import com.ibm.icu.impl.ICULocaleService.ICUResourceBundleFactory; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Random; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.TreeSet; + +public class ICUServiceThreadTest extends TestFmwk +{ + public static void main(String[] args) throws Exception { + ICUServiceThreadTest test = new ICUServiceThreadTest(); + test.run(args); + + // get + // getvisibleids + // getdisplayname(locale) + // factories + + // registerFactory + // unregisterFactory + + // 1) concurrent access + // 2) access while factories change + // 3) iteration while factories change + // 4) concurrent conflicting access + } + + private static final String[] countries = { + "ab", "bc", "cd", "de", "ef", "fg", "gh", "ji", "ij", "jk" + }; + private static final String[] languages = { + "", "ZY", "YX", "XW", "WV", "VU", "UT", "TS", "SR", "RQ", "QP" + }; + private static final String[] variants = { + "", "", "", "GOLD", "SILVER", "BRONZE" + }; + + private static class TestFactory extends SimpleFactory { + TestFactory(String id) { + super(LocaleUtility.getLocaleFromName(id), id, true); + } + + public String getDisplayName(String id, Locale locale) { + return (visible && id.equals(this.id)) ? "(" + locale.toString() + ") " + id : null; + } + + public String toString() { + return "Factory_" + id; + } + } + + private static final Random r = new Random(); + + private static String getCLV() { + String c = countries[r.nextInt(countries.length)]; + String l = languages[r.nextInt(languages.length)]; + String v = variants[r.nextInt(variants.length)]; + return new Locale(c, l, v).toString(); + } + + private static boolean WAIT = true; + private static boolean GO = false; + private static long TIME = 5000; + + public static void runThreads() { + runThreads(TIME); + } + + public static void runThreads(long time) { + try { + GO = true; + WAIT = false; + + Thread.sleep(time); + + WAIT = true; + GO = false; + + Thread.sleep(300); + } + catch (InterruptedException e) { + } + } + + static class TestThread extends Thread implements TestLog { + private final String name; + protected ICUService service; + private final long delay; + private final TestLog log; + + public TestThread(String name, ICUService service, long delay, TestLog log) { + this.name = name + " "; + this.service = service; + this.delay = delay; + this.log = log; + this.setDaemon(true); + } + + public void run() { + while (WAIT) { + Thread.yield(); + } + + try { + while (GO) { + iterate(); + if (delay > 0) { + Thread.sleep(delay); + } + } + } + catch (InterruptedException e) { + } + } + + protected void iterate() { + } + + public boolean logging() { + return log != null; + } + + public void log(String msg) { + if (logging()) { + log.log(name + msg); + } + } + + public void logln(String msg) { + if (logging()) { + log.logln(name + msg); + } + } + + public void err(String msg) { + if (logging()) { + log.err(name + msg); + } + } + + public void errln(String msg) { + if (logging()) { + log.errln(name + msg); + } + } + } + + static class RegisterFactoryThread extends TestThread { + RegisterFactoryThread(String name, ICUService service, long delay, TestLog log) { + super("REG " + name, service, delay, log); + } + + protected void iterate() { + Factory f = new TestFactory(getCLV()); + service.registerFactory(f); + logln(f.toString()); + } + } + + static class UnregisterFactoryThread extends TestThread { + private Random r; + List factories; + + UnregisterFactoryThread(String name, ICUService service, long delay, TestLog log) { + super("UNREG " + name, service, delay, log); + + r = new Random(); + factories = service.factories(); + } + + public void iterate() { + int s = factories.size(); + if (s == 0) { + factories = service.factories(); + } else { + int n = r.nextInt(s); + Factory f = (Factory)factories.remove(n); + boolean success = service.unregisterFactory(f); + if (logging()) logln("factory: " + f + (success ? " succeeded." : " *** failed.")); + } + } + } + + static class UnregisterFactoryListThread extends TestThread { + Factory[] factories; + int n; + + UnregisterFactoryListThread(String name, ICUService service, long delay, Factory[] factories, TestLog log) { + super("UNREG " + name, service, delay, log); + + this.factories = factories; + } + + public void iterate() { + if (n < factories.length) { + Factory f = factories[n++]; + boolean success = service.unregisterFactory(f); + if (logging()) logln("factory: " + f + (success ? " succeeded." : " *** failed.")); + } + } + } + + + static class GetVisibleThread extends TestThread { + GetVisibleThread(String name, ICUService service, long delay, TestLog log) { + super("VIS " + name, service, delay, log); + } + + protected void iterate() { + Set ids = service.getVisibleIDs(); + Iterator iter = ids.iterator(); + int n = 10; + while (--n >= 0 && iter.hasNext()) { + String id = (String)iter.next(); + Object result = service.get(id); + logln("iter: " + n + " id: " + id + " result: " + result); + } + } + } + + static class GetDisplayThread extends TestThread { + Locale locale; + + GetDisplayThread(String name, ICUService service, long delay, Locale locale, TestLog log) { + super("DIS " + name, service, delay, log); + + this.locale = locale; + } + + protected void iterate() { + Map names = service.getDisplayNames(locale); + Iterator iter = names.entrySet().iterator(); + int n = 10; + while (--n >= 0 && iter.hasNext()) { + Entry e = (Entry)iter.next(); + String dname = (String)e.getKey(); + String id = (String)e.getValue(); + Object result = service.get(id); + if (logging()) logln(" iter: " + n + + " dname: " + dname + + " id: " + id + + " result: " + result); + } + } + } + + static class GetThread extends TestThread { + private String[] actualID; + + GetThread(String name, ICUService service, long delay, TestLog log) { + super("GET " + name, service, delay, log); + + actualID = new String[1]; + } + + protected void iterate() { + String id = getCLV(); + Object o = service.get(id, actualID); + if (logging() && o != null) { + logln(" id: " + id + " actual: " + actualID[0] + " result: " + o); + } + } + } + + static class GetListThread extends TestThread { + private final String[] list; + private int n; + + GetListThread(String name, ICUService service, long delay, String[] list, TestLog log) { + super("GETL " + name, service, delay, log); + + this.list = list; + } + + protected void iterate() { + if (--n < 0) { + n = list.length - 1; + } + String id = list[n]; + Object o = service.get(id); + if (logging()) { + logln(" id: " + id + " result: " + o); + } + } + } + + // return a collection of unique factories, might be fewer than requested + Collection getFactoryCollection(int requested) { + Set locales = new HashSet(); + for (int i = 0; i < requested; ++i) { + locales.add(getCLV()); + } + List factories = new ArrayList(locales.size()); + Iterator iter = locales.iterator(); + while (iter.hasNext()) { + factories.add(new TestFactory((String)iter.next())); + } + return factories; + } + + void registerFactories(ICUService service, Collection c) { + Iterator iter = c.iterator(); + while (iter.hasNext()) { + service.registerFactory((Factory)iter.next()); + } + } + + ICUService stableService() { + if (stableService == null) { + stableService = new ICULocaleService(); + registerFactories(stableService, getFactoryCollection(50)); + } + return stableService; + } + private ICUService stableService; + + // run multiple get on a stable service + public void Test00_ConcurrentGet() { + for(int i = 0; i < 10; ++i) { + new GetThread("[" + Integer.toString(i) + "]", stableService(), 0, this).start(); + } + runThreads(); + logln(stableService.stats()); + } + + // run multiple getVisibleID on a stable service + public void Test01_ConcurrentGetVisible() { + for(int i = 0; i < 10; ++i) { + new GetVisibleThread("[" + Integer.toString(i) + "]", stableService(), 0, this).start(); + } + runThreads(); + logln(stableService.stats()); + } + + // run multiple getDisplayName on a stable service + public void Test02_ConcurrentGetDisplay() { + String[] localeNames = { + "en", "es", "de", "fr", "zh", "it", "no", "sv" + }; + for(int i = 0; i < localeNames.length; ++i) { + String locale = localeNames[i]; + new GetDisplayThread("[" + locale + "]", + stableService(), + 0, + LocaleUtility.getLocaleFromName(locale), + this).start(); + } + runThreads(); + logln(stableService.stats()); + } + + // run register/unregister on a service + public void Test03_ConcurrentRegUnreg() { + ICUService service = new ICULocaleService(); + for (int i = 0; i < 5; ++i) { + new RegisterFactoryThread("[" + i + "]", service, 0, this).start(); + } + for (int i = 0; i < 5; ++i) { + new UnregisterFactoryThread("[" + i + "]", service, 0, this).start(); + } + runThreads(); + logln(service.stats()); + } + + public void Test04_WitheringService() { + ICUService service = new ICULocaleService(); + + Collection fc = getFactoryCollection(50); + registerFactories(service, fc); + + Factory[] factories = (Factory[])fc.toArray(new Factory[fc.size()]); + Comparator comp = new Comparator() { + public int compare(Object lhs, Object rhs) { + return lhs.toString().compareTo(rhs.toString()); + } + }; + Arrays.sort(factories, comp); + + new GetThread("", service, 0, this).start(); + new UnregisterFactoryListThread("", service, 3, factories, this).start(); + + runThreads(2000); + logln(service.stats()); + } + + // "all hell breaks loose" + // one register and one unregister thread, delay 500ms + // two display threads with different locales, delay 500ms; + // one visible id thread, delay 50ms + // fifteen get threads, delay 0 + // run for ten seconds + public void Test05_ConcurrentEverything() { + ICUService service = new ICULocaleService(); + + new RegisterFactoryThread("", service, 500, this).start(); + + for(int i = 0; i < 15; ++i) { + new GetThread("[" + Integer.toString(i) + "]", service, 0, this).start(); + } + + new GetVisibleThread("", service, 50, this).start(); + + String[] localeNames = { + "en", "de" + }; + for(int i = 0; i < localeNames.length; ++i) { + String locale = localeNames[i]; + new GetDisplayThread("[" + locale + "]", + stableService(), + 500, + LocaleUtility.getLocaleFromName(locale), + this).start(); + } + + new UnregisterFactoryThread("", service, 500, this).start(); + + // yoweee!!! + runThreads(10000); + logln(service.stats()); + } +} diff --git a/icu4j/src/com/ibm/icu/impl/ICULocaleService.java b/icu4j/src/com/ibm/icu/impl/ICULocaleService.java new file mode 100644 index 00000000000..a6a1f8a1b1c --- /dev/null +++ b/icu4j/src/com/ibm/icu/impl/ICULocaleService.java @@ -0,0 +1,364 @@ +package com.ibm.icu.impl; + +import java.lang.ref.SoftReference; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.Set; + +import java.util.Enumeration; + +public class ICULocaleService extends ICUService { + private String fallbackLocale; + + /** + * Construct an ICULocaleService with a fallback locale string based on the current + * default locale at the time of construction. + */ + public ICULocaleService() { + fallbackLocale = LocaleUtility.canonicalLocaleString(Locale.getDefault().toString()); + } + + /** + * Construct an ICULocaleService with the provided fallback locale string. + */ + public ICULocaleService(String fallbackLocale) { + this.fallbackLocale = fallbackLocale; + } + + /** + * A subclass of Key that implements a locale fallback mechanism. + * The first locale to search for is the locale provided by the + * client, and the fallback locale to search for is the default + * locale. This is instantiated by ICULocaleService.
+ * + *Canonicalization adjusts the locale string so that the + * section before the first understore is in lower case, and the rest + * is in upper case, with no trailing underscores. + */ + public static class LocaleKey extends ICUService.Key { + private String primaryID; + private String fallbackID; + private String currentID; + + public static LocaleKey create(String primaryID, String fallbackID) { + String canonicalPrimaryID = LocaleUtility.canonicalLocaleString(primaryID); + String canonicalFallbackID = LocaleUtility.canonicalLocaleString(fallbackID); + return new LocaleKey(primaryID, canonicalPrimaryID, canonicalFallbackID); + } + + public static LocaleKey createWithCanonicalFallback(String primaryID, String canonicalFallbackID) { + String canonicalPrimaryID = LocaleUtility.canonicalLocaleString(primaryID); + return new LocaleKey(primaryID, canonicalPrimaryID, canonicalFallbackID); + } + + public static LocaleKey createWithCanonical(String canonicalPrimaryID, String canonicalFallbackID) { + return new LocaleKey(canonicalPrimaryID, canonicalPrimaryID, canonicalFallbackID); + } + + /** + * PrimaryID is the user's requested locale string in + * canonical form, fallbackID is the default locale's string + * in canonical form. + */ + protected LocaleKey(String primaryID, String canonicalPrimaryID, String canonicalFallbackID) { + super(primaryID); + + if (canonicalPrimaryID == null) { + this.primaryID = ""; + } else { + this.primaryID = canonicalPrimaryID; + } + if (this.primaryID == "") { + this.fallbackID = null; + } else { + if (canonicalFallbackID == null || this.primaryID.equals(canonicalFallbackID)) { + this.fallbackID = ""; + } else { + this.fallbackID = canonicalFallbackID; + } + } + + this.currentID = this.primaryID; + } + + /** + * Return the (canonical) original ID. + */ + public String canonicalID() { + return primaryID; + } + + /** + * Return the (canonical) current ID. + */ + public String currentID() { + return currentID; + } + + /** + * If the key has a fallback, modify the key and return true, + * otherwise return false.
+ * + *First falls back through the primary ID, then through + * the fallbackID. The final fallback is the empty string, + * unless the primary id was the empty string, in which case + * there is no fallback. + */ + public boolean fallback() { + String current = currentID(); + int x = current.lastIndexOf('_'); + if (x != -1) { + currentID = current.substring(0, x); + return true; + } + if (fallbackID != null) { + currentID = fallbackID; + fallbackID = fallbackID.length() == 0 ? null : ""; + return true; + } + currentID = null; + return false; + } + } + + /** + * This is a factory that handles multiple keys, and records + * information about the keys it handles or doesn't handle. This + * allows it to quickly filter subsequent queries on keys it has + * seen before. Subclasses implement handleCreate instead of + * create. Before updateVisibleIDs is called, it keeps track of + * keys that it doesn't handle. If its ids are visible, once + * updateVisibleIDs is called, it builds a set of all the keys it + * does handle and keeps track of the keys it does handle. + */ + public static abstract class MultipleKeyFactory implements ICUService.Factory { + protected final boolean visible; + private SoftReference cacheref; + private boolean included; + + public MultipleKeyFactory() { + this(true); + } + + public MultipleKeyFactory(boolean visible) { + this.visible = visible; + } + + /** + * Get the cache of IDs. These are either the ids that we know we + * don't understand, if included is false, or the entire set of ids + * we do know we understand, if included is true. Note that if + * the cache has been freed by gc, we reset the included flag, so + * it must not be tested before this method is called. + */ + private HashSet getCache() { + HashSet cache = null; + if (cacheref != null) { + cache = (HashSet)cacheref.get(); + } + if (cache == null) { + cache = new HashSet(); + cacheref = new SoftReference(cache); + included = false; + } + return cache; + } + + /** + * Get the cache of IDs we understand. + */ + private HashSet getIncludedCache() { + HashSet cache = getCache(); + if (!included) { + cache.clear(); + handleUpdateVisibleIDs(cache); + included = true; + } + return cache; + } + + public final Object create(Key key) { + Object result = null; + String id = key.currentID(); + HashSet cache = getCache(); + if (cache.contains(id) == included) { + result = handleCreate(key); + if (!included && result == null) { + cache.add(id); + } + } + return result; + } + + public final void updateVisibleIDs(Map result) { + if (visible) { + Set cache = getIncludedCache(); + Iterator iter = cache.iterator(); + while (iter.hasNext()) { + result.put((String)iter.next(), this); + } + } + } + + public final String getDisplayName(String id, Locale locale) { + if (visible) { + Set cache = getIncludedCache(); + if (cache.contains(id)) { + return handleGetDisplayName(id, locale); + } + } + return null; + } + + /** + * Subclasses implement this instead of create. + */ + protected abstract Object handleCreate(Key key); + + /** + * Subclasses implement this instead of updateVisibleIDs. Any + * id known to and handled by this class should be added to + * result. + */ + protected abstract void handleUpdateVisibleIDs(Set result); + + /** + * Subclasses implement this instead of getDisplayName. + * Return the display name for the (visible) id in the + * provided locale. The default implementation just returns + * the id. + */ + protected String handleGetDisplayName(String id, Locale locale) { + return id; + } + } + + /** + * A factory that creates a service based on the ICU locale data. + * Subclasses specify a prefix (default is LocaleElements), a + * semicolon-separated list of required resources, and a visible flag. + * This factory will search the ICU locale data for a bundle with + * the exact prefix. Then it will test whether the required resources + * are all in this exact bundle. If so, it instantiates the full + * resource bundle, and hands it to createServiceFromResource, which + * subclasses must implement. Otherwise it returns null. + */ + public static class ICUResourceBundleFactory extends MultipleKeyFactory { + protected final String name; + protected final String[] requiredContents; + + /** + * A service factory based on ICU resource data in the LocaleElements resources. + */ + public ICUResourceBundleFactory(String requiredContents, boolean visible) { + this(ICULocaleData.LOCALE_ELEMENTS, requiredContents, visible); + } + + /** + * A service factory based on ICU resource data in resources + * with the given name. If requiredContents is not null, all + * listed resources must come directly from the same bundle. + */ + public ICUResourceBundleFactory(String name, String requiredContents, boolean visible) { + super(visible); + + this.name = name; + if (requiredContents != null) { + ArrayList list = new ArrayList(); + for (int i = 0, len = requiredContents.length();;) { + while (i < len && requiredContents.charAt(i) == ';') { + ++i; + } + if (i == len) { + break; + } + int j = requiredContents.indexOf(';', i); + if (j == -1) { + j = len; + } + list.add(requiredContents.substring(i, j)); + i = j; + } + this.requiredContents = (String[])list.toArray(new String[list.size()]); + } else { + this.requiredContents = null; + } + } + + /** + * Overrides parent handleCreate call. Parent will filter out keys that it + * knows are not accepted by this factory before calling this method. + */ + protected Object handleCreate(Key key) { + Locale loc = LocaleUtility.getLocaleFromName(key.currentID()); + if (acceptsLocale(loc)) { + ResourceBundle bundle = ICULocaleData.getResourceBundle(name, loc); // full resource bundle tree lookup + return createFromBundle(bundle, key); + } + return null; + } + + /** + * Queries all the available locales in ICU and adds the names + * of those which it accepts to result. This is quite + * time-consuming so we don't want to do it more than once if + * we have to. This is only called if we are visible. + */ + protected void handleUpdateVisibleIDs(Set result) { + Locale[] locales = ICULocaleData.getAvailableLocales(name); + for (int i = 0; i < locales.length; ++i) { + Locale locale = locales[i]; + if (acceptsLocale(locale)) { + result.add(LocaleUtility.canonicalLocaleString(locale.toString())); + } + } + } + + /** + * Return a localized name for the locale represented by id. + */ + protected String handleGetDisplayName(String id, Locale locale) { + // use java's display name formatting for now + return LocaleUtility.getLocaleFromName(id).getDisplayName(locale); + } + + /** + * We only accept the locale if there is a bundle for this exact locale and if + * all the required resources are directly in this bundle (none is from an + * inherited bundle); + */ + protected boolean acceptsLocale(Locale loc) { + + try { + ResourceBundle bundle = ICULocaleData.loadResourceBundle(name, loc); // single resource bundle lookup + if (requiredContents != null) { + for (int i = 0; i < requiredContents.length; ++i) { + if (bundle.getObject(requiredContents[i]) == null) { + return false; + } + } + } + return true; + } + catch (Exception e) { + } + return false; + } + + /** + * Subclassers implement this to create their service object based on the bundle and key. + * The default implementation just returns the bundle. + */ + protected Object createFromBundle(ResourceBundle bundle, Key key) { + return bundle; + } + } + + protected Key createKey(String id) { + return LocaleKey.createWithCanonicalFallback(id, fallbackLocale); + } +} diff --git a/icu4j/src/com/ibm/icu/impl/ICUNotifier.java b/icu4j/src/com/ibm/icu/impl/ICUNotifier.java new file mode 100644 index 00000000000..219184bf665 --- /dev/null +++ b/icu4j/src/com/ibm/icu/impl/ICUNotifier.java @@ -0,0 +1,162 @@ +package com.ibm.icu.impl; + +import java.util.ArrayList; +import java.util.EventListener; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +/** + *
Abstract implementation of a notification facility. Clients add + * EventListeners with addListener and remove them with removeListener. + * Notifiers call notifyChanged when they wish to notify listeners. + * This queues the listener list on the notification thread, which + * eventually dequeues the list and calls notifyListener on each + * listener in the list.
+ * + *Subclasses override acceptsListener and notifyListener + * to add type-safe notification. AcceptsListener should return + * true if the listener is of the appropriate type; ICUNotifier + * itself will ensure the listener is non-null and that the + * identical listener is not already registered with the Notifier. + * NotifyListener should cast the listener to the appropriate + * type and call the appropriate method on the listener. + */ +public abstract class ICUNotifier { + private Object notifyLock = new Object(); + private NotifyThread notifyThread; + private List listeners; + + /** + * Add a listener to be notified when notifyChanged is called. + * The listener must not be null. AcceptsListener must return + * true for the listener. Attempts to concurrently + * register the identical listener more than once will be + * silently ignored. + */ + public void addListener(EventListener l) { + if (l == null) { + throw new NullPointerException(); + } + + if (acceptsListener(l)) { + synchronized (notifyLock) { + if (listeners == null) { + listeners = new ArrayList(5); + } + // identity equality check + Iterator iter = listeners.iterator(); + while (iter.hasNext()) { + if (iter.next() == l) { + return; + } + } + + listeners.add(l); + } + } else { + throw new InternalError("Listener invalid for this notifier."); + } + } + + /** + * Stop notifying this listener. The listener must + * not be null. Attemps to remove a listener that is + * not registered will be silently ignored. + */ + public void removeListener(EventListener l) { + if (l == null) { + throw new NullPointerException(); + } + synchronized (notifyLock) { + if (listeners != null) { + // identity equality check + Iterator iter = listeners.iterator(); + while (iter.hasNext()) { + if (iter.next() == l) { + iter.remove(); + if (listeners.size() == 0) { + listeners = null; + } + return; + } + } + } + } + } + + /** + * Queue a notification on the notification thread for the current + * listeners. When the thread unqueues the notification, notifyListener + * is called on each listener from the notification thread. + */ + public void notifyChanged() { + if (listeners != null) { + synchronized (notifyLock) { + if (listeners != null) { + if (notifyThread == null) { + notifyThread = new NotifyThread(); + notifyThread.setDaemon(true); + notifyThread.start(); + } + notifyThread.queue(listeners.toArray()); + } + } + } + } + + /** + * The notification thread. + */ + private class NotifyThread extends Thread { + List queue; + + /** + * Queue the notification on the thread. + */ + public void queue(Object[] list) { + synchronized (this) { + if (queue == null) { + queue = new LinkedList(); + } + queue.add(list); + notify(); + } + } + + /** + * Wait for a notification to be queued, then notify all + * listeners listed in the notification. + */ + public void run() { + Object[] list; + while (true) { + try { + synchronized (this) { + while (queue.isEmpty()) { + wait(); + } + list = (Object[])queue.remove(0); + } + + for (int i = 0; i < list.length; ++i) { + notifyListener((EventListener)list[i]); + } + } + catch (InterruptedException e) { + } + } + } + } + + /** + * Subclasses implement this to return true if the listener is + * of the appropriate type. + */ + protected abstract boolean acceptsListener(EventListener l); + + /** + * Subclasses implement this to notify the listener. + */ + protected abstract void notifyListener(EventListener l); +} diff --git a/icu4j/src/com/ibm/icu/impl/ICURWLock.java b/icu4j/src/com/ibm/icu/impl/ICURWLock.java new file mode 100644 index 00000000000..d626bda9b84 --- /dev/null +++ b/icu4j/src/com/ibm/icu/impl/ICURWLock.java @@ -0,0 +1,254 @@ +package com.ibm.icu.impl; + +// See Allan Holub's 1999 column in JavaWorld. + +import java.util.LinkedList; + +/** + *
A simple Reader/Writer lock. This assumes that there will + * be little writing contention. It also doesn't allow + * active readers to acquire and release a write lock, or + * deal with priority inversion issues.
+ * + *Access to the lock should be enclosed in a try/finally block
+ * in order to ensure that the lock is always released in case of
+ * exceptions:
+ * try { + * lock.acquireRead(); + * // use service protected by the lock + * } + * finally { + * lock.releaseRead(); + * } + *+ * + *
The lock provides utility methods getStats and clearStats + * to return statistics on the use of the lock.
+ */ +public class ICURWLock { + private LinkedList ww; // list of waiting writers + private int wwc; // waiting writers + private int rc; // active readers, -1 if there's an active writer + private int wrc; // waiting readers + + private int stat_rc; // any read access granted + private int stat_mrc; // multiple read access granted + private int stat_wrc; // wait for read + private int stat_wc; // write access granted + private int stat_wwc; // wait for write + + /** + * Internal class used to gather statistics on the RWLock. + */ + public final static class Stats { + /** + * Number of times read access granted (read count). + */ + public final int _rc; + + /** + * Number of times concurrent read access granted (multiple read count). + */ + public final int _mrc; + + /** + * Number of times blocked for read (waiting reader count). + */ + public final int _wrc; // wait for read + + /** + * Number of times write access granted (writer count). + */ + public final int _wc; + + /** + * Number of times blocked for write (waiting writer count). + */ + public final int _wwc; + + private Stats(ICURWLock lock) { + this(lock.stat_rc, lock.stat_mrc, lock.stat_wrc, lock.stat_wc, lock.stat_wwc); + } + + private Stats(int rc, int mrc, int wrc, int wc, int wwc) { + _rc = rc; _mrc = mrc; _wrc = wrc; _wc = wc; _wwc = wwc; + } + + /** + * Return a string listing all the stats. + */ + public String toString() { + return " rc: " + _rc + + " mrc: " + _mrc + + " wrc: " + _wrc + + " wc: " + _wc + + " wwc: " + _wwc; + } + } + + /** + * Reset the stats. + */ + public void clearStats() { + stat_rc = stat_mrc = stat_wrc = stat_wc = stat_wwc = 0; + } + + /** + * Return a snapshot of the current stats. This does not clear the stats. + */ + public Stats getStats() { + return new Stats(this); + } + + /** + *Acquire a read lock, blocking until a read lock is + * available. Multiple readers can concurrently hold the read + * lock.
+ * + *If there's a writer, or a waiting writer, increment the + * waiting reader count and block on this. Otherwise + * increment the active reader count and return. Caller must call + * releaseRead when done (for example, in a finally block).
+ */ + public synchronized void acquireRead() { + if (rc >= 0 && wwc == 0) { + ++rc; + ++stat_rc; + if (rc > 1) ++stat_mrc; + } else { + ++wrc; + ++stat_wrc; + try { + wait(); + } + catch (InterruptedException e) { + } + } + } + + /** + *Release a read lock and return. An error will be thrown + * if a read lock is not currently held.
+ * + *If this is the last active reader, notify the oldest + * waiting writer. Call when finished with work + * controlled by acquireRead.
+ */ + public synchronized void releaseRead() { + if (rc > 0) { + if (0 == --rc) { + notifyWaitingWriter(); + } + } else { + throw new InternalError("no current reader to release"); + } + } + + /** + *Acquire the write lock, blocking until the write lock is + * available. Only one writer can acquire the write lock, and + * when held, no readers can acquire the read lock.
+ * + *If there are no readers and no waiting writers, mark as + * having an active writer and return. Otherwise, add a lock to the + * end of the waiting writer list, and block on it. Caller + * must call releaseWrite when done (for example, in a finally + * block).
+ */ + public void acquireWrite() { + // Do a quick check up top, to save us the lock allocation and + // extra synch in the case where there is no contention for + // writing. This is common in ICUService. + + synchronized (this) { + if (rc == 0 && wwc == 0) { + rc = -1; + ++stat_wc; + return; + } + } + + // We assume at this point that there is an active reader or a + // waiting writer, so we don't recheck, though we could. + + // Create a lock for this thread only, it will be the only + // thread notified when the lock comes to the front of the + // waiting writer list. We synchronize on the lock first, and + // then this, because when we release the lock on this, the + // lock will be available to the world. If another thread + // removed and notified that lock before we synchronized and + // waited on it, we'd miss the only notification, and we would + // wait on it forever. So we synchronized on it before we + // make it available, so that we're guaranteed to be waiting on + // it before any notification can occur. + + Object lock = new Object(); + synchronized (lock) { + synchronized (this) { + // again, we've assumed we don't have multiple waiting + // writers, so we leave this null until we need it. + // Once created, we keep it around. + if (ww == null) { + ww = new LinkedList(); + } + ww.addLast(lock); + ++wwc; + ++stat_wwc; + } + try { + lock.wait(); + } + catch (InterruptedException e) { + } + } + } + + /** + *
Release the write lock and return. An error will be thrown + * if the write lock is not currently held.
+ * + *If there are waiting readers, make them all active and + * notify all of them. Otherwise, notify the oldest waiting + * writer, if any. Call when finished with work controlled by + * acquireWrite.
+ */ + public synchronized void releaseWrite() { + if (rc < 0) { + if (wrc > 0) { + rc = wrc; + wrc = 0; + if (rc > 0) { + stat_rc += rc; + if (rc > 1) { + stat_mrc += rc - 1; + } + } + notifyAll(); + } else { + rc = 0; + notifyWaitingWriter(); + } + } else { + throw new InternalError("no current writer to release"); + } + } + + /** + * If there is a waiting writer thread, mark us as active for + * writing, remove it from the list, and notify it. + */ + private void notifyWaitingWriter() { + // only called within a block synchronized on this + // we don't assume there is necessarily a waiting writer, + // no no error if there isn't. + if (wwc > 0) { + rc = -1; + Object lock = ww.removeFirst(); + --wwc; + ++stat_wc; + synchronized (lock) { + lock.notify(); + } + } + } +} diff --git a/icu4j/src/com/ibm/icu/impl/ICUService.java b/icu4j/src/com/ibm/icu/impl/ICUService.java new file mode 100644 index 00000000000..22271a57d13 --- /dev/null +++ b/icu4j/src/com/ibm/icu/impl/ICUService.java @@ -0,0 +1,649 @@ +package com.ibm.icu.impl; + +import java.lang.ref.SoftReference; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EventListener; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; + +/** + *A Service provides access to service objects that implement a + * particular service, e.g. transliterators. Users provide a String + * id (for example, a locale string) to the service, and get back an + * object for that id. Service objects can be any kind of object. + * The service object is cached and returned for later queries, so + * generally it should not be mutable, or the caller should clone the + * object before modifying it.
+ * + *Services 'canonicalize' the query id and use the canonical id + * to query for the service. The service also defines a mechanism + * to 'fallback' the id multiple times. Clients can optionally + * request the actual id that was matched by a query when they + * use an id to retrieve a service object.
+ * + *Service objects are instantiated by Factory objects registered with + * the service. The service queries each Factory in turn, from most recently + * registered to earliest registered, until one returns a service object. + * If none responds with a service object, a fallback id is generated, + * and the process repeats until a service object is returned or until + * the id has no further fallbacks.
+ * + *Factories can be dynamically registered and unregistered with the + * service. When registered, a Factory is installed at the head of + * the factory list, and so gets 'first crack' at any keys or fallback + * keys. When unregistered, it is removed from the service and can no + * longer be located through it. Service objects generated by this + * factory and held by the client are unaffected.
+ * + *Internally, ICUService uses Keys to query factories and perform + * fallback. The Key defines the canonical form of the id, and + * implements the fallback strategy. Custom Keys can be defined that + * parse complex IDs into components that Factories can more easily + * use. The Key can cache the results of this parsing to save + * repeated effort.
+ * + *ICUService implements ICUNotifier, so that clients can register + * to to receive notification when factories are added or removed from + * the service. ICUService provides a default EventListener subinterface, + * ServiceListener, which can be registered with the service. When + * the service changes, the ServiceListener's serviceChanged method + * is called, with the service as the only argument.
+ * + *The ICUService API is both rich and generic, and it is expected + * that most implementations will statically 'wrap' ICUService to + * present a more appropriate API-- for example, to declare the type + * of the objects returned from get, to limit the factories that can + * be registered with the service, or to define their own listener + * interface with a custom callback method. They might also customize + * ICUService by overriding it, for example, to customize the Key and + * fallback strategy. ICULocaleService is a customized service that + * uses Locale names as ids and uses Keys that implement the standard + * resource bundle fallback strategy.
+ */ +public class ICUService extends ICUNotifier { + /** + * Access to factories is protected by a read-write lock. This is + * to allow multiple threads to read concurrently, but keep + * changes to the factory list atomic with respect to all readers. + */ + private final ICURWLock factoryLock = new ICURWLock(); + + /** + * All the factories registered with this service. + */ + private final List factories = new ArrayList(); + + /** + * Keys define how ids are canonicalized, and determine the + * fallback strategy used when querying the factories. The default + * key just takes as its canonicalID the id assigned to it when it + * is constructed, and has no fallbacks. + */ + public static class Key { + private final String id; + + /** + * Construct a key from an id. + */ + public Key(String id) { + this.id = id; + } + + /** + * Return the original ID. + */ + public final String id() { + return id; + } + + /** + * Return the canonical version of the original ID. This implementation + * returns the original ID unchanged. + */ + public String canonicalID() { + return id; + } + + /** + * Return the (canonical) current ID. This implementation returns the + * canonical ID. + */ + public String currentID() { + return canonicalID(); + } + + /** + * If the key has a fallback, modify the key and return true, + * otherwise return false. The current ID will change if there + * is a fallback. No currentIDs should be repeated, and fallback + * must eventually return false. This implmentation has no fallbacks + * and always returns false. + */ + public boolean fallback() { + return false; + } + } + + /** + * Factories generate the service objects maintained by the + * service. A factory generates a service object from a key, + * updates id->factory mappings, and returns the display name for + * a supported id. + */ + public static interface Factory { + /** + * Create a service object from the key, if this factory + * supports the key. Otherwise, return null. + */ + public Object create(Key key); + + /** + * Add IDs understood by this factory to the result map, with + * this factory as the value. If this factory hides IDs + * currently in result, it should remove or reset the mappings + * for those IDs. + */ + public void updateVisibleIDs(Map result); + + /** + * Return the display name for this id in the provided locale. + * If the id is not visible or not defined by the factory, + * return null. + */ + public String getDisplayName(String id, Locale locale); + } + + /** + * A default implementation of factory. This provides default + * implementations for subclasses, and implements a singleton + * factory that matches a single id and returns a single + * (possible deferred-initialized) instance. If visible is + * true, updates the map passed to updateVisibleIDs with a + * mapping from id to itself. + */ + public static class SimpleFactory implements Factory { + protected Object instance; + protected String id; + protected boolean visible; + + public SimpleFactory(Object instance, String id) { + this(instance, id, true); + } + + public SimpleFactory(Object instance, String id, boolean visible) { + if (instance == null || id == null) { + throw new IllegalArgumentException("instance and id must each not be null"); + } + this.instance = instance; + this.id = id; + this.visible = visible; + } + + public Object create(Key key) { + if (id.equals(key.currentID())) { + return instance; + } + return null; + } + + public void updateVisibleIDs(Map result) { + if (visible) result.put(id, this); + } + + public String getDisplayName(String id, Locale locale) { + return (visible && id.equals(this.id)) ? id : null; + } + } + + /** + * Convenience override for get(String, String[]). + */ + public Object get(String id) { + return get(id, null); + } + + /** + *
Given an id, return a service object, and the actual id under + * which it was found. If no service object matches this id, + * return null.
+ * + *This tries each registered factory in order, and if none can + * generate a service object for the key, repeats the process with + * each fallback of the key until one returns a service object, or + * the key has no fallback.
+ */ + public Object get(String id, String[] actualIDReturn) { + if (id == null) { + throw new NullPointerException(); + } + if (factories.size() == 0) { + return null; + } + + + CacheEntry result = null; + Key key = createKey(id); + if (key != null) { + try { + // The factory list can't be modified until we're done, + // otherwise we might update the cache with an invalid result. + // The cache has to stay in synch with the factory list. + factoryLock.acquireRead(); + + Map cache = null; + SoftReference cref = cacheref; // copy so we don't need to sync on this + if (cref != null) { + cache = (Map)cref.get(); + } + if (cache == null) { + // synchronized since additions and queries on the cache must be atomic + // they can be interleaved, though + cache = Collections.synchronizedMap(new HashMap()); + cref = new SoftReference(cache); + } + + String currentID = null; + ArrayList cacheIDList = null; + boolean putInCache = false; + outer: + do { + currentID = key.currentID(); + + result = (CacheEntry)cache.get(currentID); + if (result != null) { + break outer; + } + + // first test of cache failed, so we'll have to update + // the cache if we eventually succeed. + putInCache = true; + + Iterator fi = factories.iterator(); + while (fi.hasNext()) { + Object service = ((Factory)fi.next()).create(key); + if (service != null) { + result = new CacheEntry(currentID, service); + break outer; + } + } + + // prepare to load the cache with all additional ids that + // will resolve to result, assuming we'll succeed. We + // don't want to keep querying on an id that's going to + // fallback to the one that succeeded, we want to hit the + // cache the first time next goaround. + if (cacheIDList == null) { + cacheIDList = new ArrayList(5); + } + cacheIDList.add(currentID); + + } while (key.fallback()); + + if (result != null) { + if (putInCache) { + cache.put(result.actualID, result); + if (cacheIDList != null) { + Iterator iter = cacheIDList.iterator(); + while (iter.hasNext()) { + cache.put((String)iter.next(), result); + } + } + // Atomic update. We held the read lock all this time + // so we know our cache is consistent with the factory list. + // We might stomp over a cache that some other thread + // rebuilt, but that's the breaks. They're both good. + cacheref = cref; + } + + if (actualIDReturn != null) { + actualIDReturn[0] = result.actualID; + } + + return result.service; + } + } + finally { + factoryLock.releaseRead(); + } + } + + return null; + } + private SoftReference cacheref; + + // Record the actual id for this service in the cache, so we can return it + // even if we succeed later with a different id. + private static final class CacheEntry { + String actualID; + Object service; + CacheEntry(String actualID, Object service) { + this.actualID = actualID; + this.service = service; + } + } + + /** + * Return a snapshot of the visible IDs for this service. This + * set will not change as Factories are added or removed, but the + * supported ids will, so there is no guarantee that all and only + * the ids in the returned set are visible and supported by the + * service in subsequent calls. + */ + public Set getVisibleIDs() { + return getVisibleIDMap().keySet(); + } + + /** + * Return a map from visible ids to factories. + */ + private Map getVisibleIDMap() { + Map idcache = null; + SoftReference ref = idref; + if (ref != null) { + idcache = (Map)ref.get(); + } + while (idcache == null) { + synchronized (this) { // or idref-only lock? + if (ref == idref || idref == null) { + // no other thread updated idref before we got the lock, so + // grab the factory list and update it ourselves + try { + factoryLock.acquireRead(); + + idcache = new TreeMap(String.CASE_INSENSITIVE_ORDER); + + ListIterator lIter = factories.listIterator(factories.size()); + while (lIter.hasPrevious()) { + Factory f = (Factory)lIter.previous(); + f.updateVisibleIDs(idcache); + } + idcache = Collections.unmodifiableMap(idcache); + idref = new SoftReference(idcache); + } + finally { + factoryLock.releaseRead(); + } + } else { + // another thread updated idref, but gc may have stepped + // in and undone its work, leaving idcache null. If so, + // retry. + ref = idref; + idcache = (Map)ref.get(); + } + } + } + + return idcache; + } + private SoftReference idref; + + /** + * Convenience override for getDisplayName(String, Locale) that + * uses the current default locale. + */ + public String getDisplayName(String id) { + return getDisplayName(id, Locale.getDefault()); + } + + /** + * Given a visible id, return the display name in the requested locale. + * If there is no directly supported id corresponding to this id, return + * null. + */ + public String getDisplayName(String id, Locale locale) { + Map m = getVisibleIDMap(); + Factory f = (Factory)m.get(id); + return f != null ? f.getDisplayName(id, locale) : null; + } + + /** + * Convenience override of getDisplayNames(Locale) that uses the + * current default Locale. + */ + public Map getDisplayNames() { + return getDisplayNames(Locale.getDefault()); + } + + /** + * Return a snapshot of the mapping from display names to visible + * IDs for this service. This set will not change as factories + * are added or removed, but the supported ids will, so there is + * no guarantee that all and only the ids in the returned map will + * be visible and supported by the service in subsequent calls, + * nor is there any guarantee that the current display names match + * those in the set. + */ + public Map getDisplayNames(Locale locale) { + Map dncache = null; + LocaleRef ref = dnref; + if (ref != null) { + dncache = ref.get(locale); + } + while (dncache == null) { + synchronized (this) { + if (ref == dnref || dnref == null) { + dncache = new TreeMap(String.CASE_INSENSITIVE_ORDER); + //dncache = new TreeMap(/* locale-specific collator */); + + Map m = getVisibleIDMap(); + Iterator ei = m.entrySet().iterator(); + while (ei.hasNext()) { + Entry e = (Entry)ei.next(); + String id = (String)e.getKey(); + Factory f = (Factory)e.getValue(); + dncache.put(f.getDisplayName(id, locale), id); + } + + dncache = Collections.unmodifiableMap(dncache); + dnref = new LocaleRef(dncache, locale); + } else { + ref = dnref; + dncache = ref.get(locale); + } + } + } + + return dncache; + } + private static class LocaleRef { + Locale locale; + SoftReference ref; + + LocaleRef(Map dnCache, Locale locale) { + this.locale = locale; + this.ref = new SoftReference(dnCache); + } + + Map get(Locale locale) { + if (this.locale.equals(locale)) { + return (Map)ref.get(); + } + return null; + } + } + private LocaleRef dnref; + + /** + * Return a snapshot of the currently registered factories. There + * is no guarantee that the list will still match the current + * factory list of the service subsequent to this call. + */ + public final List factories() { + try { + factoryLock.acquireRead(); + return new ArrayList(factories); + } + finally{ + factoryLock.releaseRead(); + } + } + + /** + * A convenience override of registerObject(Object, String, boolean) + * that defaults visible to true. + */ + public Factory registerObject(Object obj, String id) { + return registerObject(obj, id, true); + } + + /** + * Register an object with the provided id. The id will be + * canonicalized. The canonicalized ID will be returned by + * getVisibleIDs if visible is true. + */ + public Factory registerObject(Object obj, String id, boolean visible) { + id = createKey(id).canonicalID(); + return registerFactory(new SimpleFactory(obj, id, visible)); + } + + /** + * Register a Factory. Returns the factory if the service accepts + * the factory, otherwise returns null. The default implementation + * accepts all factories. + */ + public final Factory registerFactory(Factory factory) { + if (factory == null) { + throw new NullPointerException(); + } + try { + factoryLock.acquireWrite(); + factories.add(0, factory); + clearCaches(); + } + finally { + factoryLock.releaseWrite(); + } + notifyChanged(); + return factory; + } + + /** + * Unregister a factory. The first matching registered factory will + * be removed from the list. Returns true if a matching factory was + * removed. + */ + public final boolean unregisterFactory(Factory factory) { + if (factory == null) { + throw new NullPointerException(); + } + + boolean result = false; + try { + factoryLock.acquireWrite(); + if (factories.remove(factory)) { + result = true; + clearCaches(); + } + } + finally { + factoryLock.releaseWrite(); + } + + if (result) { + notifyChanged(); + } + return result; + } + + /** + * Reset the service to the default factories. The factory + * lock is acquired and then reInitializeFactories is called. + */ + public final void reset() { + try { + factoryLock.acquireWrite(); + reInitializeFactories(); + clearCaches(); + } + finally { + factoryLock.releaseWrite(); + } + notifyChanged(); + } + + /** + * Reinitialize the factory list to its default state. By default + * this clears the list. Subclasses can override to provide other + * default initialization of the factory list. Subclasses must + * not call this method directly, as it must only be called while + * holding write access to the factory list. + */ + protected void reInitializeFactories() { + factories.clear(); + } + + /** + * Create a key from an id. This creates a Key instance. + * Subclasses can override to define more useful keys appropriate + * to the factories they accept. + */ + protected Key createKey(String id) { + return new Key(id); + } + + /** + * Clear caches maintained by this service. Subclasses can + * override if they implement additional that need to be cleared + * when the service changes. Subclasses should generally not call + * this method directly, as it must only be called while + * synchronized on this. + */ + protected void clearCaches() { + // we don't synchronize on these because methods that use them + // copy before use, and check for changes if they modify the + // caches. + cacheref = null; + idref = null; + dnref = null; + } + + /** + * ServiceListener is the listener that ICUService provides by default. + * ICUService will notifiy this listener when factories are added to + * or removed from the service. Subclasses can provide + * different listener interfaces that extend EventListener, and modify + * acceptsListener and notifyListener as appropriate. + */ + public static interface ServiceListener extends EventListener { + public void serviceChanged(ICUService service); + } + + /** + * Return true if the listener is accepted; by default this + * requires a ServiceListener. Subclasses can override to accept + * different listeners. + */ + protected boolean acceptsListener(EventListener l) { + return l instanceof ServiceListener; + } + + /** + * Notify the listener, which by default is a ServiceListener. + * Subclasses can override to use a different listener. + */ + protected void notifyListener(EventListener l) { + ((ServiceListener)l).serviceChanged(this); + } + + /** + * Return a string describing the statistics for this service. + * This also resets the statistics. Used for debugging purposes. + */ + public String stats() { + String stats = factoryLock.getStats().toString(); + factoryLock.clearStats(); + return stats; + } +} +