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