blob: fb3ee71d100fd9b350318103135cfe3f3d2e5f55 [file] [log] [blame]
package org.unicode.cldr.test;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.unicode.cldr.util.PathStarrer;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
/**
* Cache example html strings for ExampleGenerator.
*
* Essentially, the cache simply maps from xpath+value to html.
*
* The complexity of this class is mostly for the sake of handling dependencies where the
* example for pathB+valueB depends not only on pathB and valueB, but also on the current
* <em>winning</em> values of pathA1, pathA2, ...
*
* Some examples in the cache must get cleared when a changed winning value for a path makes
* the cached examples for other paths possibly no longer correct.
* For example, let pathA = "//ldml/localeDisplayNames/languages/language[@type=\"aa\"]"
* and pathB = "//ldml/localeDisplayNames/territories/territory[@type=\"DJ\"]". The values,
* in locale fr, might be "afar" for pathA and "Djibouti" for pathB. The example for pathB
* might include "afar (Djibouti)", which depends on the values of both pathA and pathB.
*
* Each ExampleGenerator object, which is for one locale, has its own ExampleCache object.
*
* This cache is internal to each ExampleGenerator. Compare TestCache.exampleGeneratorCache,
* which is at a higher level, caching entire ExampleGenerator objects, one for each locale.
*
* Unlike TestCache.exampleGeneratorCache, this cache doesn't get cleared to conserve memory,
* only to adapt to changed winning values.
*/
class ExampleCache {
/**
* An ExampleCacheItem is a temporary container for the info
* needed to get and/or put one item in the cache.
*/
class ExampleCacheItem {
private String xpath;
private String value;
/**
* starredPath, the "starred" version of xpath, is the key for the highest level
* of the cache, which is nested.
*
* Compare starred "//ldml/localeDisplayNames/languages/language[@type=\"*\"]"
* with starless "//ldml/localeDisplayNames/languages/language[@type=\"aa\"]".
* There are fewer starred paths than starless paths.
* ExampleDependencies.dependencies has starred paths for that reason.
*/
private String starredPath = null;
/**
* The cache maps each starredPath to a pathMap, which in turn maps each starless path
* to a valueMap.
*/
private Map<String, Map<String, String>> pathMap = null;
/**
* Finally the valueMap maps the value to the example html.
*/
private Map<String, String> valueMap = null;
ExampleCacheItem(String xpath, String value) {
this.xpath = xpath;
this.value = value;
}
/**
* Get the cached example html for this item, based on its xpath and value
*
* The HTML string shows example(s) using that value for that path, for the locale
* of the ExampleGenerator we're connected to.
*
* @return the example html or null
*/
String getExample() {
if (!cachingIsEnabled) {
return null;
}
String result = null;
starredPath = pathStarrer.set(xpath);
pathMap = cache.get(starredPath);
if (pathMap != null) {
valueMap = pathMap.get(xpath);
if (valueMap != null) {
result = valueMap.get(value);
}
}
if (cacheOnly && result == NONE) {
throw new InternalError("getExampleHtml cacheOnly not found: " + xpath + ", " + value);
}
return (result == NONE) ? null : result;
}
void putExample(String result) {
if (cachingIsEnabled) {
if (pathMap == null) {
pathMap = new ConcurrentHashMap<>();
cache.put(starredPath, pathMap);
}
if (valueMap == null) {
valueMap = new ConcurrentHashMap<>();
pathMap.put(xpath, valueMap);
}
valueMap.put(value, (result == null) ? NONE : result);
}
}
}
/**
* AVOID_CLEARING_CACHE: a performance optimization. Should be true except for testing.
* Only remove keys for which the examples may be affected by this change.
*
* All paths of type “A” (i.e., all that have dependencies) have keys in ExampleDependencies.dependencies.
* For any other path given as the argument to this function, there should be no need to clear the cache.
* When there are dependencies, only remove the keys for paths that are dependent on this path.
*
* Reference: https://unicode-org.atlassian.net/browse/CLDR-13636
*/
private static final boolean AVOID_CLEARING_CACHE = true;
/**
* Avoid storing null in the cache, but do store NONE as a way to remember
* there is no example html for the given xpath and value. This is probably
* faster than calling constructExampleHtml again and again to get null every
* time, if nothing at all were stored in the cache.
*/
private static final String NONE = "\uFFFF";
/**
* The nested cache mapping is: starredPath → (starlessPath → (value → html)).
*/
private final Map<String, Map<String, Map<String, String>>> cache = new ConcurrentHashMap<>();
/**
* A clearable cache is any object that supports being cleared when a path changes.
* An example is the cache of person name samples.
*/
static interface ClearableCache {
void clear();
}
/**
* The nested cache mapping is: starredPath → ClearableCache.
* TODO: because there is no concurrent multimap, use synchronization
*/
private final Multimap<String, ClearableCache> registeredCache = HashMultimap.create();
/**
* Register other caches. This isn't done often, so synchronized should be ok.
* @return
*/
<T extends ClearableCache> T registerCache(T clearableCache, String... starredPaths) {
synchronized (registeredCache) {
for (String starredPath : starredPaths) {
registeredCache.put(starredPath, clearableCache);
}
return clearableCache;
}
}
/**
* The PathStarrer is for getting starredPath from an ordinary (starless) path.
* Inclusion of starred paths enables performance improvement with AVOID_CLEARING_CACHE.
*/
private final PathStarrer pathStarrer = new PathStarrer().setSubstitutionPattern("*");
/**
* For testing, caching can be disabled for some ExampleCaches while still
* enabled for others.
*/
private boolean cachingIsEnabled = true;
void setCachingEnabled(boolean enabled) {
cachingIsEnabled = enabled;
}
/**
* For testing, we can switch some ExampleCaches into a special "cache only"
* mode, where they will throw an exception if queried for a path+value that isn't
* already in the cache. See TestExampleGeneratorDependencies.
*/
private boolean cacheOnly = false;
void setCacheOnly(boolean only) {
this.cacheOnly = only;
}
/**
* Clear the cached examples for any paths whose examples might depend on the
* winning value of the given path, since the winning value of the given path has changed.
*
* There is no need to update the example(s) for the given path itself, since
* the cache key includes path+value and therefore each path+value has its own
* example, regardless of which value is winning. There is a need to update
* the examples for OTHER paths whose examples depend on the winning value
* of the given path.
*
* @param xpath the path whose winning value has changed
*
* Called by ExampleGenerator.updateCache
*/
void update(String xpath) {
if (AVOID_CLEARING_CACHE) {
String starredA = pathStarrer.set(xpath);
for (String starredB : ExampleDependencies.dependencies.get(starredA)) {
cache.remove(starredB);
}
// TODO clean up the synchronization
synchronized (registeredCache) {
for (ClearableCache item : registeredCache.get(starredA)) {
item.clear();
}
}
} else {
cache.clear();
}
}
}