de.codecamp.messages.shared.messageformat.IcuMessageFormatSupport Maven / Gradle / Ivy
package de.codecamp.messages.shared.messageformat;
import static java.util.stream.Collectors.toSet;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZonedDateTime;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Deque;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.SetMultimap;
import com.ibm.icu.text.MessageFormat;
import com.ibm.icu.text.MessagePatternUtil;
import com.ibm.icu.text.MessagePatternUtil.ArgNode;
import com.ibm.icu.text.MessagePatternUtil.ComplexArgStyleNode;
import com.ibm.icu.text.MessagePatternUtil.MessageContentsNode;
import com.ibm.icu.text.MessagePatternUtil.MessageContentsNode.Type;
import com.ibm.icu.text.MessagePatternUtil.MessageNode;
import com.ibm.icu.text.MessagePatternUtil.VariantNode;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.CurrencyAmount;
import de.codecamp.messages.MessageKeyWithArgs;
/**
* Implements {@link MessageFormatSupport} for {@link MessageFormat ICU's MessageFormat}.
*
* If changes are made to the checked types, this must be reflected in
* {@link de.codecamp.messages.runtime.IcuMessageArgConverter}.
*/
public class IcuMessageFormatSupport
implements
MessageFormatSupport
{
public static final String ID = "icu";
/**
* the supported Java types for each argument type of the formatter
*/
private static final SetMultimap TYPE_NAME_MAPPING = HashMultimap.create();
static
{
TYPE_NAME_MAPPING.putAll("number",
Arrays.asList(Number.class.getName(), CurrencyAmount.class.getName(),
// javax.money types
"javax.money.MonetaryAmount"));
TYPE_NAME_MAPPING.putAll("date",
Arrays.asList(Date.class.getName(), Calendar.class.getName(), Number.class.getName(),
java.util.Calendar.class.getName(),
// java.time types
LocalDate.class.getName(), LocalTime.class.getName(), LocalDateTime.class.getName(),
ZonedDateTime.class.getName(), OffsetTime.class.getName(),
OffsetDateTime.class.getName(), Instant.class.getName()));
TYPE_NAME_MAPPING.putAll("time", TYPE_NAME_MAPPING.get("date"));
TYPE_NAME_MAPPING.putAll("spellout", Arrays.asList(Number.class.getName()));
TYPE_NAME_MAPPING.putAll("ordinal", Arrays.asList(Number.class.getName()));
TYPE_NAME_MAPPING.putAll("duration", Arrays.asList(Number.class.getName()));
TYPE_NAME_MAPPING.putAll("plural", Arrays.asList(Number.class.getName()));
TYPE_NAME_MAPPING.putAll("select", Arrays.asList(String.class.getName()));
TYPE_NAME_MAPPING.putAll("choice", Arrays.asList(Number.class.getName()));
}
@Override
public boolean supportsFormat(String messageFormat)
{
return IcuMessageFormatSupport.ID.equals(messageFormat)
|| DefaultMessageFormatSupport.ID.equals(messageFormat);
}
@Override
public boolean hasArgNameSupport()
{
return true;
}
@Override
public String formatArgType(String argType)
{
if ("javax.money.MonetaryAmount".equals(argType))
{
return "currency";
}
else
{
return MessageFormatSupport.super.formatArgType(argType);
}
}
@Override
public List getArgInsertOptions(MessageKeyWithArgs key)
{
List result = new ArrayList<>();
String[] argTypes = key.getArgTypes();
String[] argNames = key.getArgNames();
for (int i = 0; i < argTypes.length; i++)
{
String label = argNames[i] + " : " + formatArgType(argTypes[i]);
String reference;
if (argNames[i] != null)
reference = "{" + argNames[i] + "}";
else
reference = "{" + i + "}";
result.add(new ArgInsert(label, reference));
}
return result;
}
@Override
public String createMessageBundleComment(MessageKeyWithArgs key)
{
String comment;
if (key.hasArgs())
{
String[] argTypes = key.getArgTypes();
String[] argNames = key.getArgNames();
StringBuilder messageArgComment = new StringBuilder();
messageArgComment.append("Arguments: ");
boolean first = true;
for (int i = 0; i < argTypes.length; i++)
{
if (first)
first = false;
else
messageArgComment.append(" | ");
messageArgComment.append(argNames[i]).append(":");
messageArgComment.append(formatArgType(argTypes[i]));
messageArgComment.append(" -> ");
messageArgComment.append("{").append(argNames[i]).append("}");
}
comment = messageArgComment.toString();
}
else
{
comment = null;
}
return comment;
}
@Override
public List checkMessage(String message, String[] argTypes, String[] argNames,
TypeChecker argTypeChecker)
{
List errors = new ArrayList<>();
MessageFormat mf;
try
{
mf = new MessageFormat(message);
}
catch (IllegalArgumentException ex)
{
errors.add(String.format("The message is not a valid ICU pattern: %s", ex.getMessage()));
return errors;
}
if (mf.usesNamedArguments())
{
Set availableArgNames = Stream.of(argNames).filter(Objects::nonNull).collect(toSet());
Deque messageNodes = new ArrayDeque<>();
messageNodes.push(MessagePatternUtil.buildMessageNode(message));
while (!messageNodes.isEmpty())
{
MessageNode messageNode = messageNodes.pop();
for (MessageContentsNode node : messageNode.getContents())
{
if (node.getType() == Type.ARG)
{
ArgNode argNode = (ArgNode) node;
if (argNode.getNumber() > -1)
{
errors.add("The message mixes named and indexed arguments.");
continue;
}
ComplexArgStyleNode complexStyle = argNode.getComplexStyle();
if (complexStyle != null)
{
for (VariantNode variantNode : complexStyle.getVariants())
{
messageNodes.push(variantNode.getMessage());
}
}
String messageArgName = argNode.getName();
if (!availableArgNames.contains(messageArgName))
{
errors.add(
String.format("The message uses an unavailable argument: %s", argNode.getName()));
continue;
}
String typeName = argNode.getTypeName();
if (typeName != null)
{
String argType = null;
for (int i = 0; i < argNames.length; i++)
{
if (messageArgName.equals(argNames[i]))
argType = argTypes[i];
}
if (argType != null)
checkTypeName(typeName, argType, argTypeChecker, errors);
}
}
}
}
}
else
{
int maxIndex = -1;
Deque messageNodes = new ArrayDeque<>();
messageNodes.push(MessagePatternUtil.buildMessageNode(message));
while (!messageNodes.isEmpty())
{
MessageNode messageNode = messageNodes.pop();
for (MessageContentsNode node : messageNode.getContents())
{
if (node.getType() == Type.ARG)
{
ArgNode argNode = (ArgNode) node;
ComplexArgStyleNode complexStyle = argNode.getComplexStyle();
if (complexStyle != null)
{
for (VariantNode variantNode : complexStyle.getVariants())
{
messageNodes.push(variantNode.getMessage());
}
}
if (argNode.getNumber() > maxIndex)
maxIndex = argNode.getNumber();
String typeName = argNode.getTypeName();
if (typeName != null)
{
String argType = null;
if (argTypes.length > argNode.getNumber())
argType = argTypes[argNode.getNumber()];
checkTypeName(typeName, argType, argTypeChecker, errors);
}
}
}
}
int usedArgCount = maxIndex + 1;
int argCount = argTypes == null ? 0 : argTypes.length;
if (usedArgCount > argCount)
{
errors.add(String.format("The message uses more arguments (%d) than are declared (%d)",
usedArgCount, argCount));
}
}
return errors;
}
/**
*
* @param typeName
* the {@link ArgNode#getTypeName() type name} of the argument in the message
* @param argType
* the (Java) type of the declared message argument
*/
private void checkTypeName(String typeName, String argType, TypeChecker argTypeChecker,
List errors)
{
Set expectedArgTypes = TYPE_NAME_MAPPING.get(typeName);
if (!argTypeChecker.isCompatibleWith(argType, expectedArgTypes))
{
errors.add(
String.format("The message format type %s does not match the expected argument types: %s",
typeName, StringUtils.join(expectedArgTypes, ",")));
}
}
}