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