xref: /aosp_15_r20/external/cldr/tools/cldr-code/src/main/java/org/unicode/cldr/api/PrefixVisitorHost.java (revision 912701f9769bb47905792267661f0baf2b85bed5)
1 package org.unicode.cldr.api;
2 
3 import static com.google.common.base.Preconditions.checkNotNull;
4 import static com.google.common.base.Preconditions.checkState;
5 import static org.unicode.cldr.api.CldrData.PathOrder.NESTED_GROUPING;
6 
7 import java.util.ArrayDeque;
8 import java.util.Deque;
9 import java.util.function.BiConsumer;
10 import java.util.function.Consumer;
11 import org.unicode.cldr.api.CldrData.PathOrder;
12 import org.unicode.cldr.api.CldrData.PrefixVisitor;
13 import org.unicode.cldr.api.CldrData.PrefixVisitor.Context;
14 import org.unicode.cldr.api.CldrData.ValueVisitor;
15 
16 /**
17  * Utility class for reconstructing nested path visitation from a sequence of path/value pairs. See
18  * {@link PrefixVisitor} for more information.
19  */
20 final class PrefixVisitorHost {
21     /**
22      * Accepts a prefix visitor over a nested sequence of prefix paths derived from the given data
23      * instance. This method synthesizes a sequence of start/end events for all the sub-trees
24      * implied by the sequence of CLDR paths produced by the data supplier.
25      *
26      * <p>For example, given the sequence:
27      *
28      * <pre>{@code
29      * //ldml/foo/bar/first
30      * //ldml/foo/bar/second
31      * //ldml/foo/bar/third
32      * //ldml/foo/baz/first
33      * //ldml/foo/baz/second
34      * //ldml/quux
35      * }</pre>
36      *
37      * the following start, end and value visitation events will be derived:
38      *
39      * <pre>{@code
40      * start: //ldml
41      * start: //ldml/foo
42      * start: //ldml/foo/bar
43      * value: //ldml/foo/bar/first
44      * value: //ldml/foo/bar/second
45      * value: //ldml/foo/bar/third
46      * end:   //ldml/foo/bar
47      * start: //ldml/foo/baz
48      * value: //ldml/foo/baz/first
49      * value: //ldml/foo/baz/second
50      * end:   //ldml/foo/baz
51      * end:   //ldml/foo
52      * value: //ldml/quux
53      * end:   //ldml
54      * }</pre>
55      *
56      * <p>Note that deriving the proper sequence of start/end events can only occur if the data is
57      * provided in at least {@link PathOrder#NESTED_GROUPING NESTED_GROUPING} order. If a lower path
58      * order (e.g. {@link PathOrder#ARBITRARY ARBITRARY}) is given then {@code NESTED_GROUPING} will
59      * be used.
60      */
accept( BiConsumer<PathOrder, ValueVisitor> acceptFn, PathOrder order, PrefixVisitor v)61     static void accept(
62             BiConsumer<PathOrder, ValueVisitor> acceptFn, PathOrder order, PrefixVisitor v) {
63         PrefixVisitorHost host = new PrefixVisitorHost(v);
64         if (order.ordinal() < NESTED_GROUPING.ordinal()) {
65             order = NESTED_GROUPING;
66         }
67         acceptFn.accept(order, host.visitor);
68         host.endVisitation();
69     }
70 
71     /**
72      * Represents the root of a sub hierarchy visitation rooted at some path prefix. VisitorState
73      * instances are kept in a stack; they are added when a new visitor is installed to begin a sub
74      * hierarchy visitation and removed automatically once the visitation is complete.
75      */
76     @SuppressWarnings("unused") // For unused arguments in no-op default methods.
77     private abstract static class VisitorState implements PrefixVisitor {
78         /** Creates a visitor state from the given visitor for the specified leaf value. */
of( T visitor, Consumer<T> doneHandler, CldrPath prefix)79         static <T extends ValueVisitor> VisitorState of(
80                 T visitor, Consumer<T> doneHandler, CldrPath prefix) {
81             return new VisitorState(prefix, () -> doneHandler.accept(visitor)) {
82                 @Override
83                 public void visitValue(CldrValue value) {
84                     visitor.visit(value);
85                 }
86             };
87         }
88 
89         /** Creates a visitor state from the given visitor rooted at the specified path prefix. */
of( T visitor, Consumer<T> doneHandler, CldrPath prefix)90         static <T extends PrefixVisitor> VisitorState of(
91                 T visitor, Consumer<T> doneHandler, CldrPath prefix) {
92             return new VisitorState(prefix, () -> doneHandler.accept(visitor)) {
93                 @Override
94                 public void visitPrefixStart(CldrPath prefix, Context ctx) {
95                     visitor.visitPrefixStart(prefix, ctx);
96                 }
97 
98                 @Override
99                 public void visitPrefixEnd(CldrPath prefix) {
100                     visitor.visitPrefixEnd(prefix);
101                 }
102 
103                 @Override
104                 public void visitValue(CldrValue value) {
105                     visitor.visitValue(value);
106                 }
107             };
108         }
109 
110         // The root of the sub hierarchy visitation.
111         /* @Nullable */ private final CldrPath prefix;
112         private final Runnable doneCallback;
113 
114         private VisitorState(CldrPath prefix, Runnable doneCallback) {
115             this.prefix = prefix;
116             this.doneCallback = doneCallback;
117         }
118     }
119 
120     // Stack of currently installed visitor.
121     private final Deque<VisitorState> visitorStack = new ArrayDeque<>();
122     /* @Nullable */ private CldrPath lastValuePath = null;
123 
124     // Visits a single value (with its path) and synthesizes prefix start/end calls according
125     // to the state of the visitor stack.
126     // This is a private field to avoid anyone accidentally calling the visit method directly.
127     private final ValueVisitor visitor =
128             value -> {
129                 CldrPath path = value.getPath();
130                 int commonLength = 0;
131                 if (lastValuePath != null) {
132                     commonLength = CldrPath.getCommonPrefixLength(lastValuePath, path);
133                     checkState(
134                             commonLength <= lastValuePath.getLength(),
135                             "unexpected child path encountered: %s is child of %s",
136                             path,
137                             lastValuePath);
138                     handleLastPath(commonLength);
139                 }
140                 // ... then down to the new path (which cannot be a parent of the old path either).
141                 checkState(
142                         commonLength <= path.getLength(),
143                         "unexpected parent path encountered: %s is parent of %s",
144                         path,
145                         lastValuePath);
146                 recursiveStartVisit(path.getParent(), commonLength, new PrefixContext());
147                 // This is a no-op if the head of the stack is a prefix visitor.
148                 visitorStack.peek().visitValue(value);
149                 lastValuePath = path;
150             };
151 
152     private PrefixVisitorHost(PrefixVisitor visitor) {
153         this.visitorStack.push(VisitorState.of(visitor, v -> {}, null));
154     }
155 
156     // Called after visitation is complete to close out the last visited value path.
157     private void endVisitation() {
158         if (lastValuePath != null) {
159             handleLastPath(0);
160         }
161     }
162 
163     // Recursively visits new prefix path elements (from top-to-bottom) for a new sub hierarchy.
164     private void recursiveStartVisit(
165             /* @Nullable */ CldrPath prefix, int commonLength, PrefixContext ctx) {
166         if (prefix != null && prefix.getLength() > commonLength) {
167             recursiveStartVisit(prefix.getParent(), commonLength, ctx);
168             // Get the current visitor here (it could have been modified by the call above).
169             // This is a no-op if the head of the stack is a value visitor.
170             visitorStack.peek().visitPrefixStart(prefix, ctx.setPrefix(prefix));
171         }
172     }
173 
174     // Go up from the previous path to the common length (we _have_ already visited the leaf
175     // node of the previous path and we do not allow the new path to be a sub-path) ...
176     private void handleLastPath(int length) {
177         for (CldrPath prefix = lastValuePath.getParent();
178                 prefix != null && prefix.getLength() > length;
179                 prefix = prefix.getParent()) {
180             // Get the current visitor here (it could have been modified by the last iteration).
181             //
182             // Note: e.prefix can be null for the top-most entry in the stack, but that's fine
183             // since it will never match "prefix" and we never want to remove it anyway.
184             VisitorState e = visitorStack.peek();
185             if (prefix.equals(e.prefix)) {
186                 e.doneCallback.run();
187                 visitorStack.pop();
188                 e = visitorStack.peek();
189             }
190             // This is a no-op if the head of the stack is a value visitor.
191             e.visitPrefixEnd(prefix);
192         }
193     }
194 
195     /**
196      * Implements a reusable context which captures the current prefix being processed. This is used
197      * if a visitor wants to install a sub-visitor at a particular point during visitation.
198      */
199     private final class PrefixContext implements Context {
200         // Only null until first use.
201         private CldrPath prefix = null;
202 
203         // Must be called immediately prior to visiting a prefix visitor.
204         private PrefixContext setPrefix(CldrPath prefix) {
205             this.prefix = checkNotNull(prefix);
206             return this;
207         }
208 
209         @Override
210         public <T extends PrefixVisitor> void install(T visitor, Consumer<T> doneHandler) {
211             visitorStack.push(VisitorState.of(visitor, doneHandler, prefix));
212         }
213 
214         @Override
215         public <T extends ValueVisitor> void install(T visitor, Consumer<T> doneHandler) {
216             visitorStack.push(VisitorState.of(visitor, doneHandler, prefix));
217         }
218     }
219 }
220