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