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

com.github.leeonky.dal.runtime.RuntimeContextBuilder Maven / Gradle / Ivy

package com.github.leeonky.dal.runtime;

import com.github.leeonky.dal.ast.node.DALNode;
import com.github.leeonky.dal.format.Formatter;
import com.github.leeonky.dal.runtime.inspector.Inspector;
import com.github.leeonky.dal.runtime.inspector.InspectorBuilder;
import com.github.leeonky.dal.runtime.inspector.ValueInspector;
import com.github.leeonky.dal.runtime.schema.Expect;
import com.github.leeonky.dal.type.ExtensionName;
import com.github.leeonky.dal.type.Schema;
import com.github.leeonky.interpreter.RuntimeContext;
import com.github.leeonky.interpreter.SyntaxException;
import com.github.leeonky.util.BeanClass;
import com.github.leeonky.util.Converter;
import com.github.leeonky.util.InvocationException;
import com.github.leeonky.util.NumberType;

import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.github.leeonky.dal.runtime.schema.Actual.actual;
import static com.github.leeonky.dal.runtime.schema.Verification.expect;
import static com.github.leeonky.util.BeanClass.create;
import static java.lang.String.format;
import static java.lang.reflect.Modifier.STATIC;
import static java.util.Arrays.stream;
import static java.util.Collections.emptySet;
import static java.util.stream.Collectors.joining;

public class RuntimeContextBuilder {
    private final ClassKeyMap> propertyAccessors = new ClassKeyMap<>();
    private final ClassKeyMap> listAccessors = new ClassKeyMap<>();
    private final ClassKeyMap> objectImplicitMapper = new ClassKeyMap<>();
    private final Map valueConstructors = new LinkedHashMap<>();
    private final Map> schemas = new HashMap<>();
    private final Set extensionMethods = new HashSet<>();
    private final Map> metaProperties = new HashMap<>();
    private final List userDefinedLiterals = new ArrayList<>();
    private final NumberType numberType = new NumberType();
    private final Map, List>> curryingMethodArgRanges = new HashMap<>();
    private final Map textFormatterMap = new LinkedHashMap<>();
    private Converter converter = Converter.getInstance();
    private final ClassKeyMap equalsCheckers = new ClassKeyMap<>();
    private final ClassKeyMap matchesCheckers = new ClassKeyMap<>();
    private final ClassKeyMap inspectorBuilders = new ClassKeyMap<>();

    public RuntimeContextBuilder registerMetaProperty(Object property, Function function) {
        metaProperties.put(property, function);
        return this;
    }

    public RuntimeContextBuilder registerTextFormatter(String name, TextFormatter formatter) {
        textFormatterMap.put(name, formatter);
        return this;
    }

    public DALRuntimeContext build(Object inputValue) {
        return new DALRuntimeContext(inputValue);
    }

    public RuntimeContextBuilder registerValueFormat(Formatter formatter) {
        return registerValueFormat(formatter.getFormatterName(), formatter);
    }

    @SuppressWarnings("unchecked")
    public RuntimeContextBuilder registerValueFormat(String name, Formatter formatter) {
        valueConstructors.put(name, (o, c) -> ((Formatter) formatter).transform(o.getInstance()));
        return this;
    }

    public RuntimeContextBuilder registerSchema(Class schema) {
        return registerSchema(NameStrategy.SIMPLE_NAME, schema);
    }

    @SuppressWarnings("unchecked")
    public RuntimeContextBuilder registerSchema(String name, Class schema) {
        schemas.put(name, create(schema));
        return registerSchema(name, (data, context) ->
                expect(new Expect(create((Class) schema), null)).verify(context, actual(data)));
    }

    public RuntimeContextBuilder registerSchema(String name, BiFunction predicate) {
        valueConstructors.put(name, (o, context) -> {
            if (predicate.apply(o, context))
                return o.getInstance();
            throw new IllegalTypeException();
        });
        return this;
    }

    @SuppressWarnings("unchecked")
    public  RuntimeContextBuilder registerPropertyAccessor(Class type, PropertyAccessor propertyAccessor) {
        propertyAccessors.put(type, (PropertyAccessor) propertyAccessor);
        return this;
    }

    @SuppressWarnings("unchecked")
    public  RuntimeContextBuilder registerListAccessor(Class type, ListAccessor listAccessor) {
        listAccessors.put(type, (ListAccessor) listAccessor);
        return this;
    }

    public RuntimeContextBuilder registerSchema(NameStrategy nameStrategy, Class schema) {
        return registerSchema(nameStrategy.toName(schema), schema);
    }

    public RuntimeContextBuilder registerStaticMethodExtension(Class staticMethodExtensionClass) {
        Stream.of(staticMethodExtensionClass.getMethods()).filter(method -> method.getParameterCount() >= 1
                && (STATIC & method.getModifiers()) != 0).forEach(extensionMethods::add);
        return this;
    }

    @SuppressWarnings("unchecked")
    public  RuntimeContextBuilder registerImplicitData(Class type, Function mapper) {
        objectImplicitMapper.put(type, (Function) mapper);
        return this;
    }

    public Converter getConverter() {
        return converter;
    }

    public RuntimeContextBuilder setConverter(Converter converter) {
        this.converter = converter;
        return this;
    }

    public RuntimeContextBuilder registerUserDefinedLiterals(UserLiteralRule rule) {
        userDefinedLiterals.add(rule);
        return this;
    }

    public RuntimeContextBuilder registerCurryingMethodRange(Method method, BiFunction, List> range) {
        curryingMethodArgRanges.put(method, range);
        return this;
    }

    private Set methodToCurrying(Class type, Object methodName) {
        return Stream.of(stream(type.getMethods()).filter(method -> !Modifier.isStatic(method.getModifiers()))
                                .filter(method -> method.getName().equals(methodName)),
                        staticMethodsToCurrying(type, methodName, Object::equals),
                        staticMethodsToCurrying(type, methodName, Class::isAssignableFrom))
                .flatMap(Function.identity()).collect(Collectors.toCollection(LinkedHashSet::new));
    }

    private Stream staticMethodsToCurrying(Class type, Object property,
                                                   BiPredicate, Class> condition) {
        return extensionMethods.stream()
                .filter(method -> staticExtensionMethodName(method).equals(property))
                .filter(method -> condition.test(method.getParameters()[0].getType(), type));
    }

    static String staticExtensionMethodName(Method method) {
        ExtensionName extensionName = method.getAnnotation(ExtensionName.class);
        return extensionName != null ? extensionName.value() : method.getName();
    }

    BiFunction, List> fetchCurryingMethodArgRange(Method method) {
        return curryingMethodArgRanges.get(method);
    }

    public RuntimeContextBuilder registerMatchesChecker(Class type, Checker checker) {
        matchesCheckers.put(type, checker);
        return this;
    }

    public RuntimeContextBuilder registerEqualsChecker(Class type, Checker checker) {
        equalsCheckers.put(type, checker);
        return this;
    }

    public RuntimeContextBuilder registerValueInspector(Class... types) {
        for (Class type : types)
            registerInspector(type, ValueInspector::new);
        return this;
    }

    public RuntimeContextBuilder registerInspector(Class type, InspectorBuilder builder) {
        inspectorBuilders.put(type, builder);
        return this;
    }

    public class DALRuntimeContext implements RuntimeContext {
        private final LinkedList stack = new LinkedList<>();
        private final Map partialPropertyStacks;

        public DALRuntimeContext(Object inputValue) {
            stack.push(wrap(inputValue));
            partialPropertyStacks = new HashMap<>();
        }

        public Data getThis() {
            return stack.getFirst();
        }

        public  T newBlockScope(Data data, Supplier supplier) {
            try {
                stack.push(data);
                return supplier.get();
            } finally {
                stack.pop();
            }
        }

        public Optional searchValueConstructor(String type) {
            return Optional.ofNullable(valueConstructors.get(type));
        }

        public Set findPropertyReaderNames(Object instance) {
            return propertyAccessors.getData(instance).getPropertyNames(instance);
        }

        public Boolean isNull(Object instance) {
            return propertyAccessors.tryGetData(instance).map(f -> f.isNull(instance))
                    .orElseGet(() -> Objects.equals(instance, null));
        }

        public Object getPropertyValue(Data data, Object property) {
            PropertyAccessor propertyAccessor = propertyAccessors.getData(data.getInstance());
            try {
                return propertyAccessor.getValueByData(data, property);
            } catch (InvalidPropertyException e) {
                return data.currying(property).orElseThrow(() -> e).resolve();
            } catch (InvocationException e) {
                throw e;
            } catch (Exception e) {
                throw new InvocationException(e);
            }
        }

        @SuppressWarnings("unchecked")
        public Iterable getList(Object instance) {
            return listAccessors.tryGetData(instance).map(l -> (Iterable) l.toIterable(instance))
                    .orElseGet(() -> arrayIterable(instance));
        }

        public int getListFirstIndex(Object instance) {
            return listAccessors.tryGetData(instance).map(listAccessor -> listAccessor.firstIndex(instance)).orElse(0);
        }

        private Iterable arrayIterable(Object instance) {
            return () -> new Iterator() {
                private final int length = Array.getLength(instance);
                private int index = 0;

                @Override
                public boolean hasNext() {
                    return index < length;
                }

                @Override
                public Object next() {
                    return Array.get(instance, index++);
                }
            };
        }

        public boolean isRegisteredList(Object instance) {
            return listAccessors.tryGetData(instance).map(listAccessor -> listAccessor.isList(instance)).orElse(false);
        }

        public Converter getConverter() {
            return converter;
        }

        public Data wrap(Object instance) {
            return new Data(instance, this, SchemaType.createRoot());
        }

        public Data wrap(Object instance, String schema, boolean isList) {
            BeanClass schemaBeanClass = schemas.get(schema);
            if (isList)
                schemaBeanClass = create(Array.newInstance(schemaBeanClass.getType(), 0).getClass());
            return new Data(instance, this, SchemaType.create(schemaBeanClass));
        }

        public  DALRuntimeContext registerPropertyAccessor(T instance) {
            if (!Objects.equals(instance, null) && !propertyAccessors.containsType(instance))
                propertyAccessors.put(BeanClass.getClass(instance),
                        new JavaClassPropertyAccessor<>(BeanClass.createFrom(instance)));
            return this;
        }

        public Optional takeUserDefinedLiteral(String token) {
            return userDefinedLiterals.stream().map(userLiteralRule -> userLiteralRule.compile(token))
                    .filter(Result::hasResult)
                    .findFirst();
        }

        public void appendPartialPropertyReference(Data data, Object symbol) {
            fetchPartialProperties(data).map(partialProperties -> partialProperties.appendPartialProperties(symbol));
        }

        private Optional fetchPartialProperties(Data data) {
            return partialPropertyStacks.values().stream().map(partialPropertyStack ->
                    partialPropertyStack.fetchPartialProperties(data)).filter(Objects::nonNull).findFirst();
        }

        public void initPartialPropertyStack(Data instance, Object prefix, Data partial) {
            partialPropertyStacks.computeIfAbsent(instance, _key -> fetchPartialProperties(instance)
                    .map(partialProperties -> partialProperties.partialPropertyStack)
                    .orElseGet(PartialPropertyStack::new)).setupPartialProperties(prefix, partial);
        }

        public Set collectPartialProperties(Data instance) {
            PartialPropertyStack partialPropertyStack = partialPropertyStacks.get(instance);
            if (partialPropertyStack != null)
                return partialPropertyStack.collectPartialProperties(instance);
            return fetchPartialProperties(instance).map(partialProperties ->
                    partialProperties.partialPropertyStack.collectPartialProperties(instance)).orElse(emptySet());
        }

        public NumberType getNumberType() {
            return numberType;
        }

        public Optional getImplicitObject(Object obj) {
            return objectImplicitMapper.tryGetData(obj).map(mapper -> mapper.apply(obj));
        }

        public Set methodToCurrying(Class type, Object methodName) {
            return RuntimeContextBuilder.this.methodToCurrying(type, methodName);
        }

        public Function fetchMetaFunction(DALNode property) {
            return metaProperties.computeIfAbsent(property.getRootSymbolName(), k -> {
                throw new RuntimeException(format("Meta property `%s` not found", property.getRootSymbolName()),
                        property.getPositionBegin());
            });
        }

        public TextFormatter fetchFormatter(String name, int position) {
            return textFormatterMap.computeIfAbsent(name, attribute -> {
                throw new SyntaxException(format("Invalid text formatter `%s`, all supported formatters are:\n%s",
                        attribute, textFormatterMap.entrySet().stream().map(e -> format("  %s:\n    %s",
                                e.getKey(), e.getValue().description())).collect(joining("\n"))), position);
            });
        }

        public Checker fetchEqualsChecker(ExpectActual expectActual) {
            return equalsCheckers.tryGetData(expectActual.getExpectInstance())
                    .orElse(ConditionalChecker.EQUALS_CHECKER);
        }

        public Checker fetchMatchesChecker(ExpectActual expectActual) {
            return matchesCheckers.tryGetData(expectActual.getExpectInstance())
                    .orElseGet(expectActual::defaultMatchesChecker);
        }

        public Inspector fetchInspector(Data data) {
            return inspectorBuilders.tryGetData(data.getInstance()).orElseGet(() -> Inspector::defaultInspector).apply(data);
        }
    }
}