com.ibm.icu.message2.MFDataModelValidator Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of icu4j Show documentation
Show all versions of icu4j Show documentation
International Component for Unicode for Java (ICU4J) is a mature, widely used Java library
providing Unicode and Globalization support
// © 2024 and later: Unicode, Inc. and others.
// License & terms of use: https://www.unicode.org/copyright.html
package com.ibm.icu.message2;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.StringJoiner;
import com.ibm.icu.message2.MFDataModel.Annotation;
import com.ibm.icu.message2.MFDataModel.CatchallKey;
import com.ibm.icu.message2.MFDataModel.Declaration;
import com.ibm.icu.message2.MFDataModel.Expression;
import com.ibm.icu.message2.MFDataModel.FunctionAnnotation;
import com.ibm.icu.message2.MFDataModel.FunctionExpression;
import com.ibm.icu.message2.MFDataModel.InputDeclaration;
import com.ibm.icu.message2.MFDataModel.Literal;
import com.ibm.icu.message2.MFDataModel.LiteralExpression;
import com.ibm.icu.message2.MFDataModel.LiteralOrCatchallKey;
import com.ibm.icu.message2.MFDataModel.LiteralOrVariableRef;
import com.ibm.icu.message2.MFDataModel.LocalDeclaration;
import com.ibm.icu.message2.MFDataModel.Option;
import com.ibm.icu.message2.MFDataModel.PatternMessage;
import com.ibm.icu.message2.MFDataModel.SelectMessage;
import com.ibm.icu.message2.MFDataModel.VariableExpression;
import com.ibm.icu.message2.MFDataModel.VariableRef;
import com.ibm.icu.message2.MFDataModel.Variant;
// I can merge all this in the MFDataModel class and make it private
class MFDataModelValidator {
private final MFDataModel.Message message;
private final Set declaredVars = new HashSet<>();
MFDataModelValidator(MFDataModel.Message message) {
this.message = message;
}
boolean validate() throws MFParseException {
if (message instanceof PatternMessage) {
validateDeclarations(((PatternMessage) message).declarations);
} else if (message instanceof SelectMessage) {
SelectMessage sm = (SelectMessage) message;
validateDeclarations(sm.declarations);
validateSelectors(sm.selectors);
int selectorCount = sm.selectors.size();
validateVariants(sm.variants, selectorCount);
}
return true;
}
private boolean validateVariants(List variants, int selectorCount)
throws MFParseException {
if (variants == null || variants.isEmpty()) {
error("Selection messages must have at least one variant");
}
// Look for an entry with all keys = '*'
boolean hasUltimateFallback = false;
Set fakeKeys = new HashSet<>();
for (Variant variant : variants) {
if (variant.keys == null || variant.keys.isEmpty()) {
error("Selection variants must have at least one key");
}
if (variant.keys.size() != selectorCount) {
error("Selection variants must have the same number of variants as the selectors.");
}
int catchAllCount = 0;
StringJoiner fakeKey = new StringJoiner("<<::>>");
for (LiteralOrCatchallKey key : variant.keys) {
if (key instanceof CatchallKey) {
catchAllCount++;
fakeKey.add("*");
} else if (key instanceof Literal) {
fakeKey.add(((Literal) key).value);
}
}
if (fakeKeys.contains(fakeKey.toString())) {
error("Dumplicate combination of keys");
} else {
fakeKeys.add(fakeKey.toString());
}
if (catchAllCount == selectorCount) {
hasUltimateFallback = true;
}
}
if (!hasUltimateFallback) {
error("There must be one variant with all the keys being '*'");
}
return true;
}
private boolean validateSelectors(List selectors) throws MFParseException {
if (selectors == null || selectors.isEmpty()) {
error("Selection messages must have selectors");
}
return true;
}
/*
* .input {$foo :number} .input {$foo} => ERROR
* .input {$foo :number} .local $foo={$bar} => ERROR, local foo overrides an input
* .local $foo={...} .local $foo={...} => ERROR, foo declared twice
* .local $a={$foo} .local $b={$foo} => NOT AN ERROR (foo is used, not declared)
* .local $a={:f opt=$foo} .local $foo={$foo} => ERROR, foo declared after beeing used in opt
*/
private boolean validateDeclarations(List declarations) throws MFParseException {
if (declarations == null || declarations.isEmpty()) {
return true;
}
for (Declaration declaration : declarations) {
if (declaration instanceof LocalDeclaration) {
LocalDeclaration ld = (LocalDeclaration) declaration;
validateExpression(ld.value, false);
addVariableDeclaration(ld.name);
} else if (declaration instanceof InputDeclaration) {
InputDeclaration id = (InputDeclaration) declaration;
validateExpression(id.value, true);
}
}
return true;
}
/*
* One might also consider checking if the same variable is used with more than one type:
* .local $a = {$foo :number}
* .local $b = {$foo :string}
* .local $c = {$foo :datetime}
*
* But this is not necesarily an error.
* If $foo is a number, then it might be formatter as a number, or as date (epoch time),
* or something else.
*
* So it is not safe to complain. Especially with custom functions:
* # get the first name from a `Person` object
* .local $b = {$person :getField fieldName=firstName}
* # get formats a `Person` object
* .local $b = {$person :person}
*/
private void validateExpression(Expression expression, boolean fromInput)
throws MFParseException {
String argName = null;
Annotation annotation = null;
if (expression instanceof Literal) {
// ...{foo}... or ...{|foo|}... or ...{123}...
// does not declare anything
} else if (expression instanceof LiteralExpression) {
LiteralExpression le = (LiteralExpression) expression;
argName = le.arg.value;
annotation = le.annotation;
} else if (expression instanceof VariableExpression) {
VariableExpression ve = (VariableExpression) expression;
// ...{$foo :bar opt1=|str| opt2=$x opt3=$y}...
// .input {$foo :number} => declares `foo`, if already declared is an error
// .local $a={$foo} => declares `a`, but only used `foo`, does not declare it
argName = ve.arg.name;
annotation = ve.annotation;
} else if (expression instanceof FunctionExpression) {
// ...{$foo :bar opt1=|str| opt2=$x opt3=$y}...
FunctionExpression fe = (FunctionExpression) expression;
annotation = fe.annotation;
}
if (annotation instanceof FunctionAnnotation) {
FunctionAnnotation fa = (FunctionAnnotation) annotation;
if (fa.options != null) {
for (Option opt : fa.options.values()) {
LiteralOrVariableRef val = opt.value;
if (val instanceof VariableRef) {
// We had something like {:f option=$val}, it means we's seen `val`
// It is not a declaration, so not an error.
addVariableDeclaration(((VariableRef) val).name);
}
}
}
}
// We chech the argument name after options to prevent errors like this:
// .local $foo = {$a :b option=$foo}
if (argName != null) {
// if we come from `.input {$foo :function}` then `varName` is null
// and `argName` is `foo`
if (fromInput) {
addVariableDeclaration(argName);
} else {
// Remember that we've seen it, to complain if there is a declaration later
declaredVars.add(argName);
}
}
}
private boolean addVariableDeclaration(String varName) throws MFParseException {
if (declaredVars.contains(varName)) {
error("Variable '" + varName + "' already declared");
return false;
}
declaredVars.add(varName);
return true;
}
private void error(String text) throws MFParseException {
throw new MFParseException(text, -1);
}
}