1 package org.unicode.cldr.test; 2 3 import java.util.Map; 4 import java.util.concurrent.ConcurrentHashMap; 5 6 import org.unicode.cldr.util.PathStarrer; 7 8 import com.google.common.collect.HashMultimap; 9 import com.google.common.collect.Multimap; 10 11 /** 12 * Cache example html strings for ExampleGenerator. 13 * 14 * Essentially, the cache simply maps from xpath+value to html. 15 * 16 * The complexity of this class is mostly for the sake of handling dependencies where the 17 * example for pathB+valueB depends not only on pathB and valueB, but also on the current 18 * <em>winning</em> values of pathA1, pathA2, ... 19 * 20 * Some examples in the cache must get cleared when a changed winning value for a path makes 21 * the cached examples for other paths possibly no longer correct. 22 23 * For example, let pathA = "//ldml/localeDisplayNames/languages/language[@type=\"aa\"]" 24 * and pathB = "//ldml/localeDisplayNames/territories/territory[@type=\"DJ\"]". The values, 25 * in locale fr, might be "afar" for pathA and "Djibouti" for pathB. The example for pathB 26 * might include "afar (Djibouti)", which depends on the values of both pathA and pathB. 27 * 28 * Each ExampleGenerator object, which is for one locale, has its own ExampleCache object. 29 * 30 * This cache is internal to each ExampleGenerator. Compare TestCache.exampleGeneratorCache, 31 * which is at a higher level, caching entire ExampleGenerator objects, one for each locale. 32 * 33 * Unlike TestCache.exampleGeneratorCache, this cache doesn't get cleared to conserve memory, 34 * only to adapt to changed winning values. 35 */ 36 class ExampleCache { 37 /** 38 * An ExampleCacheItem is a temporary container for the info 39 * needed to get and/or put one item in the cache. 40 */ 41 class ExampleCacheItem { 42 private String xpath; 43 private String value; 44 45 /** 46 * starredPath, the "starred" version of xpath, is the key for the highest level 47 * of the cache, which is nested. 48 * 49 * Compare starred "//ldml/localeDisplayNames/languages/language[@type=\"*\"]" 50 * with starless "//ldml/localeDisplayNames/languages/language[@type=\"aa\"]". 51 * There are fewer starred paths than starless paths. 52 * ExampleDependencies.dependencies has starred paths for that reason. 53 */ 54 private String starredPath = null; 55 56 /** 57 * The cache maps each starredPath to a pathMap, which in turn maps each starless path 58 * to a valueMap. 59 */ 60 private Map<String, Map<String, String>> pathMap = null; 61 62 /** 63 * Finally the valueMap maps the value to the example html. 64 */ 65 private Map<String, String> valueMap = null; 66 ExampleCacheItem(String xpath, String value)67 ExampleCacheItem(String xpath, String value) { 68 this.xpath = xpath; 69 this.value = value; 70 } 71 72 /** 73 * Get the cached example html for this item, based on its xpath and value 74 * 75 * The HTML string shows example(s) using that value for that path, for the locale 76 * of the ExampleGenerator we're connected to. 77 * 78 * @return the example html or null 79 */ getExample()80 String getExample() { 81 if (!cachingIsEnabled) { 82 return null; 83 } 84 String result = null; 85 starredPath = pathStarrer.set(xpath); 86 pathMap = cache.get(starredPath); 87 if (pathMap != null) { 88 valueMap = pathMap.get(xpath); 89 if (valueMap != null) { 90 result = valueMap.get(value); 91 } 92 } 93 if (cacheOnly && result == NONE) { 94 throw new InternalError("getExampleHtml cacheOnly not found: " + xpath + ", " + value); 95 } 96 return (result == NONE) ? null : result; 97 } 98 putExample(String result)99 void putExample(String result) { 100 if (cachingIsEnabled) { 101 if (pathMap == null) { 102 pathMap = new ConcurrentHashMap<>(); 103 cache.put(starredPath, pathMap); 104 } 105 if (valueMap == null) { 106 valueMap = new ConcurrentHashMap<>(); 107 pathMap.put(xpath, valueMap); 108 } 109 valueMap.put(value, (result == null) ? NONE : result); 110 } 111 } 112 } 113 114 /** 115 * AVOID_CLEARING_CACHE: a performance optimization. Should be true except for testing. 116 * Only remove keys for which the examples may be affected by this change. 117 * 118 * All paths of type “A” (i.e., all that have dependencies) have keys in ExampleDependencies.dependencies. 119 * For any other path given as the argument to this function, there should be no need to clear the cache. 120 * When there are dependencies, only remove the keys for paths that are dependent on this path. 121 * 122 * Reference: https://unicode-org.atlassian.net/browse/CLDR-13636 123 */ 124 private static final boolean AVOID_CLEARING_CACHE = true; 125 126 /** 127 * Avoid storing null in the cache, but do store NONE as a way to remember 128 * there is no example html for the given xpath and value. This is probably 129 * faster than calling constructExampleHtml again and again to get null every 130 * time, if nothing at all were stored in the cache. 131 */ 132 private static final String NONE = "\uFFFF"; 133 134 /** 135 * The nested cache mapping is: starredPath → (starlessPath → (value → html)). 136 */ 137 private final Map<String, Map<String, Map<String, String>>> cache = new ConcurrentHashMap<>(); 138 139 /** 140 * A clearable cache is any object that supports being cleared when a path changes. 141 * An example is the cache of person name samples. 142 */ 143 static interface ClearableCache { clear()144 void clear(); 145 } 146 147 /** 148 * The nested cache mapping is: starredPath → ClearableCache. 149 * TODO: because there is no concurrent multimap, use synchronization 150 */ 151 152 private final Multimap<String, ClearableCache> registeredCache = HashMultimap.create(); 153 154 /** 155 * Register other caches. This isn't done often, so synchronized should be ok. 156 * @return 157 */ 158 registerCache(T clearableCache, String... starredPaths)159 <T extends ClearableCache> T registerCache(T clearableCache, String... starredPaths) { 160 synchronized (registeredCache) { 161 for (String starredPath : starredPaths) { 162 registeredCache.put(starredPath, clearableCache); 163 } 164 return clearableCache; 165 } 166 } 167 168 /** 169 * The PathStarrer is for getting starredPath from an ordinary (starless) path. 170 * Inclusion of starred paths enables performance improvement with AVOID_CLEARING_CACHE. 171 */ 172 private final PathStarrer pathStarrer = new PathStarrer().setSubstitutionPattern("*"); 173 174 /** 175 * For testing, caching can be disabled for some ExampleCaches while still 176 * enabled for others. 177 */ 178 private boolean cachingIsEnabled = true; 179 setCachingEnabled(boolean enabled)180 void setCachingEnabled(boolean enabled) { 181 cachingIsEnabled = enabled; 182 } 183 184 /** 185 * For testing, we can switch some ExampleCaches into a special "cache only" 186 * mode, where they will throw an exception if queried for a path+value that isn't 187 * already in the cache. See TestExampleGeneratorDependencies. 188 */ 189 private boolean cacheOnly = false; 190 setCacheOnly(boolean only)191 void setCacheOnly(boolean only) { 192 this.cacheOnly = only; 193 } 194 195 /** 196 * Clear the cached examples for any paths whose examples might depend on the 197 * winning value of the given path, since the winning value of the given path has changed. 198 * 199 * There is no need to update the example(s) for the given path itself, since 200 * the cache key includes path+value and therefore each path+value has its own 201 * example, regardless of which value is winning. There is a need to update 202 * the examples for OTHER paths whose examples depend on the winning value 203 * of the given path. 204 * 205 * @param xpath the path whose winning value has changed 206 * 207 * Called by ExampleGenerator.updateCache 208 */ update(String xpath)209 void update(String xpath) { 210 if (AVOID_CLEARING_CACHE) { 211 String starredA = pathStarrer.set(xpath); 212 for (String starredB : ExampleDependencies.dependencies.get(starredA)) { 213 cache.remove(starredB); 214 } 215 // TODO clean up the synchronization 216 synchronized (registeredCache) { 217 for (ClearableCache item : registeredCache.get(starredA)) { 218 item.clear(); 219 } 220 } 221 } else { 222 cache.clear(); 223 } 224 } 225 } 226