blob: 66781a5c5f66b95f9e626b77aceaf29fd05f6adc [file] [log] [blame]
// © 2019 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
package org.unicode.icu.tool.cldrtoicu.mapper;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.unicode.icu.tool.cldrtoicu.IcuData;
import org.unicode.icu.tool.cldrtoicu.PathValueTransformer.Result;
import org.unicode.icu.tool.cldrtoicu.RbPath;
import org.unicode.icu.tool.cldrtoicu.RbValue;
import com.google.common.collect.ListMultimap;
/**
* An abstract parent class for any mappers based on {@code PathValueTransformer}. This ensures
* that transformation results are correctly processed when being added to IcuData instances.
*/
public abstract class AbstractPathValueMapper {
private static final Pattern ARRAY_INDEX = Pattern.compile("(/[^\\[]++)(?:\\[(\\d++)])?$");
private final IcuData icuData;
AbstractPathValueMapper(String name, boolean hasFallback) {
this.icuData = new IcuData(name, hasFallback);
}
/** Implemented by sub-classes to return all results to be added to the IcuData instance. */
abstract ListMultimap<RbPath, Result> getResults();
/**
* Adds results to the IcuData instance according to expected {@code PathValueTransformer}
* semantics. This method must only be called once per mapper.
*/
final IcuData transform() {
checkState(icuData.getPaths().isEmpty(),
"transform() method cannot be called multiple times: %s", icuData);
// This subclass mostly exists to control the fact that results need to be added in one go
// to the IcuData because of how referenced paths are handled. If results could be added in
// multiple passes, you could have confusing situations in which values has path references
// in them but the referenced paths have not been transformed yet. Forcing the subclass to
// implement a single method to generate all results at once ensures that we control the
// lifecycle of the data and how results are processed as they are added to the IcuData.
addResults(getResults());
return icuData;
}
/**
* Adds transformation results on the specified multi-map to this data instance. Results are
* handled differently according to whether they are grouped, or represent an alias value. If
* the value of an ungrouped result is itself a resource bundle path (including possibly having
* an array index) then the referenced value is assumed to be an existing path whose value is
* then substituted.
*/
// TODO: Fix this to NOT implicitly rely of ordering of referenced values.
private void addResults(ListMultimap<RbPath, Result> resultsByRbPath) {
for (RbPath rbPath : resultsByRbPath.keySet()) {
for (Result r : resultsByRbPath.get(rbPath)) {
if (r.isGrouped()) {
// Grouped results have all the values in a single value entry.
icuData.add(rbPath, RbValue.of(r.getValues()));
} else {
if (rbPath.getSegment(rbPath.length() - 1).endsWith(":alias")) {
r.getValues().forEach(v -> icuData.add(rbPath, RbValue.of(v)));
} else {
// Ungrouped results are one value per entry, but might be expanded into
// grouped results if they are a path referencing a grouped entry.
r.getValues().forEach(v -> icuData.add(rbPath, replacePathValues(v)));
}
}
}
}
}
/**
* Replaces an ungrouped CLDR value for the form "/foo/bar" or "/foo/bar[N]" which is assumed
* to be a reference to an existing value in a resource bundle. Note that the referenced bundle
* might be grouped (i.e. an array with more than one element).
*/
private RbValue replacePathValues(String value) {
Matcher m = ARRAY_INDEX.matcher(value);
if (!m.matches()) {
return RbValue.of(value);
}
// The only constraint is that the "path" value starts with a leading '/', but parsing into
// the RbPath ignores this. We must use "parse()" here, rather than RbPath.of(), since the
// captured value contains '/' characters to represent path delimiters.
RbPath replacePath = RbPath.parse(m.group(1));
List<RbValue> replaceValues = icuData.get(replacePath);
checkArgument(replaceValues != null, "Path %s is missing from IcuData", replacePath);
// If no index is given (e.g. "/foo/bar") then treat it as index 0 (i.e. "/foo/bar[0]").
int replaceIndex = m.groupCount() > 1 ? Integer.parseInt(m.group(2)) : 0;
return replaceValues.get(replaceIndex);
}
}