From 3d6edab4155a10db61d2c4b302c6867dcfe72a70 Mon Sep 17 00:00:00 2001
From: Doug Felt
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; + } +} +