xref: /aosp_15_r20/external/cldr/tools/cldr-code/src/main/java/org/unicode/cldr/test/ExampleCache.java (revision 912701f9769bb47905792267661f0baf2b85bed5)
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