de.sayayi.lib.message.parser.MessageCompiler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of message-format Show documentation
Show all versions of message-format Show documentation
Highly configurable message format library supporting message definition through annotations
The newest version!
/*
* Copyright 2020 Jeroen Gremmen
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.sayayi.lib.message.parser;
import de.sayayi.lib.antlr4.AbstractAntlr4Parser;
import de.sayayi.lib.antlr4.AbstractVocabulary;
import de.sayayi.lib.antlr4.syntax.GenericSyntaxErrorFormatter;
import de.sayayi.lib.message.Message;
import de.sayayi.lib.message.MessageFactory;
import de.sayayi.lib.message.exception.MessageParserException;
import de.sayayi.lib.message.internal.CompoundMessage;
import de.sayayi.lib.message.internal.EmptyMessage;
import de.sayayi.lib.message.internal.TextMessage;
import de.sayayi.lib.message.part.MessagePart;
import de.sayayi.lib.message.part.TemplatePart;
import de.sayayi.lib.message.part.TextPart;
import de.sayayi.lib.message.part.parameter.ParameterConfig;
import de.sayayi.lib.message.part.parameter.ParameterPart;
import de.sayayi.lib.message.part.parameter.key.*;
import de.sayayi.lib.message.part.parameter.key.ConfigKey.CompareType;
import de.sayayi.lib.message.part.parameter.value.*;
import de.sayayi.lib.message.util.SpacesUtil;
import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.intellij.lang.annotations.Language;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;
import static de.sayayi.lib.message.exception.MessageParserException.Type.MESSAGE;
import static de.sayayi.lib.message.exception.MessageParserException.Type.TEMPLATE;
import static de.sayayi.lib.message.parser.MessageLexer.BOOL;
import static de.sayayi.lib.message.parser.MessageLexer.COLON;
import static de.sayayi.lib.message.parser.MessageLexer.COMMA;
import static de.sayayi.lib.message.parser.MessageLexer.EMPTY;
import static de.sayayi.lib.message.parser.MessageLexer.EQ;
import static de.sayayi.lib.message.parser.MessageLexer.GT;
import static de.sayayi.lib.message.parser.MessageLexer.GTE;
import static de.sayayi.lib.message.parser.MessageLexer.LT;
import static de.sayayi.lib.message.parser.MessageLexer.LTE;
import static de.sayayi.lib.message.parser.MessageLexer.NAME;
import static de.sayayi.lib.message.parser.MessageLexer.NE;
import static de.sayayi.lib.message.parser.MessageLexer.NULL;
import static de.sayayi.lib.message.parser.MessageLexer.NUMBER;
import static de.sayayi.lib.message.parser.MessageLexer.P_END;
import static de.sayayi.lib.message.parser.MessageLexer.P_START;
import static de.sayayi.lib.message.parser.MessageParser.CH;
import static de.sayayi.lib.message.parser.MessageParser.DQ_END;
import static de.sayayi.lib.message.parser.MessageParser.DQ_START;
import static de.sayayi.lib.message.parser.MessageParser.L_PAREN;
import static de.sayayi.lib.message.parser.MessageParser.R_PAREN;
import static de.sayayi.lib.message.parser.MessageParser.SQ_END;
import static de.sayayi.lib.message.parser.MessageParser.SQ_START;
import static de.sayayi.lib.message.parser.MessageParser.TPL_END;
import static de.sayayi.lib.message.parser.MessageParser.TPL_START;
import static de.sayayi.lib.message.parser.MessageParser.*;
import static java.lang.Boolean.parseBoolean;
import static java.lang.Character.isSpaceChar;
import static java.lang.Integer.parseInt;
import static java.util.Objects.requireNonNull;
import static java.util.function.Function.identity;
import static java.util.stream.Collector.Characteristics.IDENTITY_FINISH;
import static java.util.stream.Collector.Characteristics.UNORDERED;
import static java.util.stream.Collectors.toList;
import static org.antlr.v4.runtime.Token.EOF;
/**
* This class provides methods for compiling messages and templates.
*
* @author Jeroen Gremmen
* @since 0.5.0
*/
@SuppressWarnings("UnknownLanguage")
public final class MessageCompiler extends AbstractAntlr4Parser
{
private final @NotNull MessageFactory messageFactory;
public MessageCompiler(@NotNull MessageFactory messageFactory)
{
super(ErrorFormatter.INSTANCE);
this.messageFactory = requireNonNull(messageFactory, "messageFactory must not be null");
}
/**
* Compile the given message {@code text} into a spaces aware message object.
*
* @param text message text, not {@code null}
*
* @return compiled message, never {@code null}
*
* @throws MessageParserException in case the message could not be parsed
*/
@Contract(pure = true)
public @NotNull Message.WithSpaces compileMessage(
@NotNull @Language("MessageFormat") String text) {
return compileMessage(text, false);
}
/**
* Compile the given template {@code text} into a spaces aware message object.
*
* @param text template text, not {@code null}
*
* @return compiled template, never {@code null}
*
* @throws MessageParserException in case the template could not be parsed
*/
@Contract(pure = true)
public @NotNull Message.WithSpaces compileTemplate(
@NotNull @Language("MessageFormat") String text) {
return compileMessage(text, true);
}
@Contract(pure = true)
private @NotNull Message.WithSpaces compileMessage(
@NotNull @Language("MessageFormat") String text, boolean template)
{
final Listener listener = new Listener(template);
try {
return parse(new Lexer(text),
lexer -> new Parser(listener.tokenStream = new BufferedTokenStream(lexer)),
Parser::message, listener, ctx -> requireNonNull(ctx.messageWithSpaces));
} catch(MessageParserException ex) {
throw ex.withType(template ? TEMPLATE : MESSAGE);
}
}
@Override
protected @NotNull RuntimeException createException(
@NotNull Token startToken, @NotNull Token stopToken, @NotNull String formattedMessage,
@NotNull String errorMsg, Exception cause) {
return new MessageParserException(errorMsg, formattedMessage, cause);
}
private static final class Lexer extends MessageLexer
{
public Lexer(@NotNull String message) {
super(CharStreams.fromString(message));
}
@Override
public Vocabulary getVocabulary() {
return MessageCompiler.VOCABULARY;
}
}
private static final class Parser extends MessageParser
{
public Parser(@NotNull TokenStream tokenStream) {
super(tokenStream);
}
@Override
public Vocabulary getVocabulary() {
return MessageCompiler.VOCABULARY;
}
}
private final class Listener extends MessageParserBaseListener
{
private final boolean template;
private TokenStream tokenStream;
private Listener(boolean template) {
this.template = template;
}
@Override
public void exitMessage(MessageContext ctx) {
ctx.messageWithSpaces = ctx.message0().messageWithSpaces;
}
@Override
public void exitMessage0(Message0Context ctx)
{
if (ctx.children == null)
ctx.messageWithSpaces = EmptyMessage.INSTANCE;
else
{
final List parts = new ArrayList<>();
for(final ParseTree part: ctx.children)
{
if (part instanceof ParameterPartContext)
parts.add(((ParameterPartContext)part).part);
else if (part instanceof TextPartContext)
parts.add(((TextPartContext)part).part);
else
{
if (template)
syntaxError((TemplatePartContext)part, "no nested template allowed");
parts.add(((TemplatePartContext)part).part);
}
}
final int partCount = parts.size();
if (partCount == 0)
ctx.messageWithSpaces = EmptyMessage.INSTANCE;
else if (partCount == 1 && parts.get(0) instanceof TextPart)
ctx.messageWithSpaces = new TextMessage((TextPart)parts.get(0));
else
{
parts.removeIf(this::exitMessage0_isRedundantTextPart);
ctx.messageWithSpaces = new CompoundMessage(parts);
}
}
}
@Contract(pure = true)
private boolean exitMessage0_isRedundantTextPart(@NotNull MessagePart messagePart)
{
if (messagePart instanceof TextPart)
{
final TextPart textPart = (TextPart)messagePart;
return "".equals(textPart.getText()) && textPart.isSpaceAround();
}
return false;
}
@Override
public void exitTextPart(TextPartContext ctx)
{
ctx.part = messageFactory
.getMessagePartNormalizer()
.normalize(new TextPart(ctx.text().characters));
}
@Override
public void exitText(TextContext ctx)
{
final List chNodes = ctx.CH();
final char[] text = new char[chNodes.size()];
int n = 0;
for(final TerminalNode chNode: chNodes)
{
final String chText = chNode.getText();
char ch = chText.charAt(0);
if (ch == '\\')
{
// handle escape characters
ch = chText.length() == 2
? chText.charAt(1)
: (char)parseInt(chText.substring(2), 16);
}
if (!isSpaceChar(ch))
text[n++] = ch;
else if (n == 0 || !isSpaceChar(text[n - 1]))
text[n++] = ' ';
}
ctx.characters = new String(text, 0, n);
}
@Override
public void exitQuotedMessage(QuotedMessageContext ctx) {
ctx.messageWithSpaces = ctx.message0().messageWithSpaces;
}
@Override
public void exitQuotedString(QuotedStringContext ctx)
{
final TextContext text = ctx.text();
ctx.string = text == null ? "" : text.characters;
}
@Override
public void exitSimpleString(SimpleStringContext ctx)
{
final NameOrKeywordContext nameOrKeyword = ctx.nameOrKeyword();
ctx.string = nameOrKeyword != null ? nameOrKeyword.name : ctx.quotedString().string;
}
@Override
public void exitForceQuotedMessage(ForceQuotedMessageContext ctx)
{
final QuotedMessageContext quotedMessage = ctx.quotedMessage();
if (quotedMessage != null)
ctx.messageWithSpaces = quotedMessage.messageWithSpaces;
else
{
//noinspection LanguageMismatch
ctx.messageWithSpaces = messageFactory.parseMessage(ctx.simpleString().string);
}
}
final Collector,Map>
PARAMETER_CONFIG_COLLECTOR = new Collector<>() {
@Override public Supplier