All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.babyfish.jimmer.sql.fetcher.compiler.FetcherCompiler Maven / Gradle / Ivy

There is a newer version: 0.8.180
Show newest version
package org.babyfish.jimmer.sql.fetcher.compiler;

import org.antlr.v4.runtime.*;
import org.babyfish.jimmer.meta.ImmutableProp;
import org.babyfish.jimmer.meta.TargetLevel;
import org.babyfish.jimmer.sql.Entity;
import org.babyfish.jimmer.sql.fetcher.*;
import org.babyfish.jimmer.sql.fetcher.impl.FetcherImpl;
import org.babyfish.jimmer.sql.fetcher.impl.FetcherImplementor;

import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Collectors;

public class FetcherCompiler {

    private static final Object JAVA_CODE_VALUE = new Object();

    private FetcherCompiler() {}

    public static Fetcher compile(String code) {
        return compile(code, null, null);
    }

    public static Fetcher compile(String code, ClassLoader classLoader) {
        return compile(code, classLoader, null);
    }

    @SuppressWarnings("unchecked")
    public static  Fetcher compile(String code, Class type) {
        return (Fetcher) compile(
                code,
                null,
                Objects.requireNonNull(type, "The argument `type` cannot be null")
        );
    }

    @SuppressWarnings("unchecked")
    private static Fetcher compile(String code, ClassLoader classLoader, Class type) {
        FetcherLexer lexer = new FetcherLexer(
                new ANTLRInputStream(code)
        );
        lexer.removeErrorListeners();
        lexer.addErrorListener(new ErrorListenerImpl());
        FetcherParser parser = new FetcherParser(
                new CommonTokenStream(lexer)
        );
        parser.removeErrorListeners();
        parser.addErrorListener(new ErrorListenerImpl());

        FetcherParser.FetcherContext ctx = parser.fetcher();
        FetcherImplementor fetcher = new FetcherImpl<>((Class) type(ctx, classLoader, type));
        fetcher = addFields(fetcher, ctx.body);
        return fetcher;
    }

    private static Class type(FetcherParser.FetcherContext ctx, ClassLoader classLoader, Class type) {
        Class javaType;
        if (ctx.type == null) {
            if (type == null) {
                throw new FetcherCompileException(
                        "The argument `type` must be specified for fetcher code without javaType name prefix",
                        ctx.start.getLine(),
                        ctx.start.getCharPositionInLine()
                );
            }
            if (!type.isInterface() || !type.isAnnotationPresent(Entity.class)) {
                throw new IllegalArgumentException(
                        "The argument `type` is \"" +
                                type.getName() +
                                "\" which is not entity java type"
                );
            }
            return type;
        }
        List parts = ctx.type.parts;
        String typeName = parts
                .stream().map(Token::getText)
                .collect(Collectors.joining("."));

        if (type != null && typeName.equals(type.getName())) {
            javaType = type;
        } else {
            try {
                javaType = classLoader != null ?
                        Class.forName(typeName, true, classLoader) :
                        Class.forName(typeName);
            } catch (ClassNotFoundException ex) {
                throw new FetcherCompileException(
                        "There is no javaType \"" +
                                typeName +
                                "\"",
                        ex,
                        parts.get(0).getLine(),
                        parts.get(0).getCharPositionInLine()
                );
            }
            if (type != null && !type.isAssignableFrom(javaType)) {
                throw new FetcherCompileException(
                        "The fetcher whose javaType is \"" +
                                typeName +
                                "\" cannot be convert to the fetcher whose javaType is \"" +
                                type.getName() +
                                "\"",
                        parts.get(0).getLine(),
                        parts.get(1).getCharPositionInLine()
                );
            }
        }
        if (!javaType.isInterface() || !javaType.isAnnotationPresent(Entity.class)) {
            throw new FetcherCompileException(
                    "The \"" +
                            typeName +
                            "\" is not entity java type",
                    parts.get(0).getLine(),
                    parts.get(0).getCharPositionInLine()
            );
        }
        return javaType;
    }

    private static FetcherImplementor addFields(FetcherImplementor fetcher, FetcherParser.FetchBodyContext body) {
        for (FetcherParser.FieldContext field : body.fields) {
            fetcher = addField(fetcher, field);
        }
        return fetcher;
    }

    @SuppressWarnings("unchecked")
    private static FetcherImplementor addField(FetcherImplementor fetcher, FetcherParser.FieldContext field) {
        String propName = field.prop.getText();
        ImmutableProp prop = fetcher.getImmutableType().getProps().get(propName);
        if (prop == null) {
            throw new FetcherCompileException(
                    "There is no property \"" +
                            propName +
                            "\" declared in \"" +
                            fetcher.getImmutableType() +
                            "\"",
                    field.prop.getLine(),
                    field.prop.getCharPositionInLine()
            );
        }
        if (prop.isId()) {
            return fetcher;
        }
        int limit = Integer.MAX_VALUE;
        int offset = 0;
        int depth = 0;
        int batchSize = 0;
        boolean recursive = false;
        for (FetcherParser.ArgumentContext argument : field.arguments) {
            Object value = parseValue(argument.value.getText());
            if (value == null) {
                throw new FetcherCompileException(
                        "Illegal value expression \"" +
                                argument.value.getText(),
                        argument.value.getLine(),
                        argument.value.getCharPositionInLine()
                );
            }
            switch (argument.name.getText()) {
                case "batchSize":
                    if (!(value instanceof Integer)) {
                        throw new FetcherCompileException(
                                "Illegal value of `batchSize` must be integer",
                                argument.value.getLine(),
                                argument.value.getCharPositionInLine()
                        );
                    }
                    batchSize = (Integer)value;
                    break;
                case "limit":
                    if (!(value instanceof Integer)) {
                        throw new FetcherCompileException(
                                "Illegal value of `limit` must be integer",
                                argument.value.getLine(),
                                argument.value.getCharPositionInLine()
                        );
                    }
                    limit = (Integer)value;
                    break;
                case "offset":
                    if (!(value instanceof Integer)) {
                        throw new FetcherCompileException(
                                "Illegal value of `offset` must be integer",
                                argument.value.getLine(),
                                argument.value.getCharPositionInLine()
                        );
                    }
                    offset = (Integer)value;
                    break;
                case "depth":
                    if (!(value instanceof Integer)) {
                        throw new FetcherCompileException(
                                "Illegal value of `depth` must be integer",
                                argument.value.getLine(),
                                argument.value.getCharPositionInLine()
                        );
                    }
                    depth = (Integer)value;
                    break;
                case "recursive":
                    if (value == JAVA_CODE_VALUE) {
                        throw new FetcherCompileException.CodeBasedRecursionException(
                                "For fetcher which will be serialized and sent to remote microservice, " +
                                        "argument value of `recursive` cannot be java code",
                                argument.value.getLine(),
                                argument.value.getCharPositionInLine()
                        );
                    }
                    if (!(value instanceof Boolean)) {
                        throw new FetcherCompileException(
                                "Illegal value of `recursive` must be integer",
                                argument.value.getLine(),
                                argument.value.getCharPositionInLine()
                        );
                    }
                    recursive = (Boolean) value;
                    break;
                case "filter":
                    throw new FetcherCompileException.CodeBasedFilterException(
                            "For fetcher which will be serialized and sent to remote microservice, " +
                                    "argument `filter` is not supported",
                            argument.value.getLine(),
                            argument.value.getCharPositionInLine()
                    );
                default:
                    throw new FetcherCompileException(
                            "Unsupported fetcher field argument \"" +
                                    argument.name.getText() +
                                    "\"",
                            argument.value.getLine(),
                            argument.value.getCharPositionInLine()
                    );
            }
        }
        if ((batchSize != 0 || limit != Integer.MAX_VALUE || offset != 0) &&
                !prop.isAssociation(TargetLevel.PERSISTENT)) {
            throw new FetcherCompileException(
                    "The field argument \"batchSize\", \"limit\" or \"offset\" " +
                            "can not be specified for non-remote association \"" +
                            prop +
                            "\"",
                    field.prop.getLine(),
                    field.prop.getCharPositionInLine()
            );
        }
        if ((depth != 0 || recursive) && prop.getTargetType() != prop.getDeclaringType()) {
            throw new FetcherCompileException(
                    "The field argument \"depth\" or \"recursive\" can not be specified for non-recursive association \"" +
                            prop +
                            "\"",
                    field.prop.getLine(),
                    field.prop.getCharPositionInLine()
            );
        }
        FetcherImplementor childFetcher = null;
        FetcherParser.FetchBodyContext body = field.body;
        if (body != null) {
            if (!prop.isAssociation(TargetLevel.ENTITY) || (prop.isTransient() && !prop.hasTransientResolver())) {
                throw new FetcherCompileException(
                        "The child fetcher can not be specified for non-association property \"" +
                                prop +
                                "\"",
                        field.prop.getLine(),
                        field.prop.getCharPositionInLine()
                );
            }
            childFetcher = new FetcherImpl<>((Class) prop.getTargetType().getJavaClass());
            childFetcher = addFields(childFetcher, body);
        }
        if (depth == 0 && !recursive && childFetcher == null) {
            return fetcher.add(propName);
        }
        return fetcher.add(
                propName,
                childFetcher,
                cfgBlock(
                        batchSize,
                        limit,
                        offset,
                        depth,
                        recursive
                )
        );
    }

    private static Object parseValue(String value) {
        if ("".equals(value)) {
            return JAVA_CODE_VALUE;
        }
        if ("true".equals(value)) {
            return true;
        }
        if ("false".equals(value)) {
            return false;
        }
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException ex) {
            return null;
        }
    }

    private static Consumer> cfgBlock(
            int batchSize,
            int limit,
            int offset,
            int depth,
            boolean recursive
    ) {
        if (batchSize == 0 && limit == Integer.MAX_VALUE && offset == 0 && depth == 0 && !recursive) {
            return null;
        }
        return cfg -> {
            if (batchSize != 0) {
                cfg.batch(batchSize);
            }
            if (limit != Integer.MAX_VALUE || offset != 0) {
                if ((long)offset + limit > Integer.MAX_VALUE) {
                    ((ListFieldConfig)cfg).limit(Integer.MAX_VALUE - offset, offset);
                } else {
                    ((ListFieldConfig) cfg).limit(limit, offset);
                }
            }
            if (depth != 0) {
                ((RecursiveFieldConfig)cfg).depth(depth);
            }
            if (recursive) {
                ((RecursiveFieldConfig)cfg).depth(Integer.MAX_VALUE);
            }
        };
    }

    private static class ErrorListenerImpl extends BaseErrorListener {

        @Override
        public void syntaxError(
                Recognizer recognizer,
                Object offendingSymbol,
                int line,
                int charPositionInLine,
                String msg,
                RecognitionException ex
        ) {
            throw new FetcherCompileException(
                    msg,
                    ex,
                    line,
                    charPositionInLine
            );
        }
    }
}