ICU-1949 ICUService

initial check in of icuservice code

X-SVN-Rev: 8902
This commit is contained in:
Doug Felt 2002-06-18 18:21:44 +00:00
parent 31903ee436
commit 3d6edab415
7 changed files with 2641 additions and 0 deletions

View file

@ -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;
}
}
}

View file

@ -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);
}
}
}

View file

@ -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());
}
}

View file

@ -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.</p>
*
* <p>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.</p>
*
* <p>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);
}
}

View file

@ -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;
/**
* <p>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.</p>
*
* <p>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);
}

View file

@ -0,0 +1,254 @@
package com.ibm.icu.impl;
// See Allan Holub's 1999 column in JavaWorld.
import java.util.LinkedList;
/**
* <p>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.</p>
*
* <p>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:<br><pre>
* try {
* lock.acquireRead();
* // use service protected by the lock
* }
* finally {
* lock.releaseRead();
* }
* </pre></p>
*
* <p>The lock provides utility methods getStats and clearStats
* to return statistics on the use of the lock.</p>
*/
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);
}
/**
* <p>Acquire a read lock, blocking until a read lock is
* available. Multiple readers can concurrently hold the read
* lock.</p>
*
* <p>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).</p>
*/
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) {
}
}
}
/**
* <p>Release a read lock and return. An error will be thrown
* if a read lock is not currently held.</p>
*
* <p>If this is the last active reader, notify the oldest
* waiting writer. Call when finished with work
* controlled by acquireRead.</p>
*/
public synchronized void releaseRead() {
if (rc > 0) {
if (0 == --rc) {
notifyWaitingWriter();
}
} else {
throw new InternalError("no current reader to release");
}
}
/**
* <p>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.</p>
*
* <p>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).<p>
*/
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) {
}
}
}
/**
* <p>Release the write lock and return. An error will be thrown
* if the write lock is not currently held.</p>
*
* <p>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.</p>
*/
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();
}
}
}
}

View file

@ -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;
/**
* <p>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.</p>
*
* <p>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.</p>
*
* <p>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.</p>
*
* <p>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.</p>
*
* <p>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.</p>
*
* <p>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.</p>
*
* <p>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.<p>
*/
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);
}
/**
* <p>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.</p>
*
* <p>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.</p>
*/
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;
}
}