Intuitive DSL for Java

Version 2.0.0 · src/main/java/ch/dbalabs/intuitivedsl/binding/DslBinder.java

Git clone
git clone https://www.dbalabs.ch/git/intuitive-dsl-java.git

DslBinder.java

/*
 * This file is part of the Intuitive DSL project.
 * Copyright (c) 2026 DBA Labs - Switzerland. All rights reserved.
 *
 * This program is dual-licensed under a commercial license and the AGPLv3.
 * For commercial licensing, contact us at [email protected] or visit https://www.dbalabs.ch.
 *
 * AGPLv3 licensing:
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, version 3 (19 November 2007).
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/agpl-3.0.html>.
 */

package ch.dbalabs.intuitivedsl.binding;

import ch.dbalabs.intuitivedsl.annotation.Bind;
import ch.dbalabs.intuitivedsl.annotation.OnClause;
import ch.dbalabs.intuitivedsl.exception.DslDefinitionException;
import ch.dbalabs.intuitivedsl.parser.ParseResult;
import ch.dbalabs.intuitivedsl.parser.ParseResult.BoundParameter;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

public class DslBinder {

    private final List<BindTarget> bindTargets;
    private final List<OnClauseTarget> clauseTargets;
    private final ConcurrentMap<Class<?>, TypeConverter<?>> converters = new ConcurrentHashMap<>();

    private record BindTarget(String paramName, String context, MethodHandle handle, Class<?> type, boolean isMethod) {}
    private record OnClauseTarget(String keyword, MethodHandle handle, Class<?> type, boolean isMethod) {}
    private record TargetCache(List<BindTarget> bindTargets, List<OnClauseTarget> clauseTargets) {}

    public DslBinder(Class<?> targetClass) {
        registerDefaultConverters();
        TargetCache cache = cacheHandles(targetClass);
        this.bindTargets = List.copyOf(cache.bindTargets());
        this.clauseTargets = List.copyOf(cache.clauseTargets());
    }

    public List<String> getBindTargetNames() {
        return bindTargets.stream().map(BindTarget::paramName).toList();
    }

    public List<String> getBindTargetContexts() {
        return bindTargets.stream().map(BindTarget::context).toList();
    }

    public List<String> getClauseTargetNames() {
        return clauseTargets.stream().map(OnClauseTarget::keyword).toList();
    }

    public <T> void registerConverter(Class<T> type, TypeConverter<T> converter) {
        converters.put(type, converter);
    }

    public void bind(Object instance, ParseResult result) {
        for (OnClauseTarget target : clauseTargets) {
            try {
                if (!target.isMethod()) {
                    // Keep field-backed clause flags deterministic even for wrapper types.
                    // Primitive booleans are already false by default; wrapper Booleans would
                    // otherwise remain null when the clause is absent.
                    target.handle().invoke(instance, false);
                }

                if (result.hasKeyword(target.keyword())) {
                    if (target.isMethod()) {
                        target.handle().invoke(instance);
                    } else {
                        target.handle().invoke(instance, true);
                    }
                }
            } catch (Throwable t) {
                throw new RuntimeException("OnClause binding failed", t);
            }
        }

        for (BindTarget target : bindTargets) {
            List<BoundParameter> matches = findMatching(target, result.getParameters());
            for (BoundParameter p : matches) {
                try {
                    Object val = convert(p.value(), target.type());
                    target.handle().invoke(instance, val);
                } catch (Throwable t) {
                    throw new RuntimeException(
                            "Failed to bind parameter '" + target.paramName() + "': " + t.getMessage(),
                            t
                    );
                }
            }
        }
    }

    private List<BoundParameter> findMatching(BindTarget target, List<BoundParameter> parameters) {
        return parameters.stream()
                .filter(p -> p.name().equalsIgnoreCase(target.paramName()))
                .filter(p -> target.context().isEmpty() || target.context().equalsIgnoreCase(p.context()))
                .toList();
    }

    private Object convert(String value, Class<?> type) throws Exception {
        TypeConverter<?> conv = converters.get(type);
        if (conv == null) {
            throw new DslDefinitionException("No converter for " + type.getName());
        }
        return conv.convert(value);
    }

    private TargetCache cacheHandles(Class<?> clazz) {
        try {
            MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(clazz, MethodHandles.lookup());
            List<BindTarget> bindTargets = new ArrayList<>();
            List<OnClauseTarget> clauseTargets = new ArrayList<>();

            for (Field f : clazz.getDeclaredFields()) {
                if (f.isAnnotationPresent(Bind.class)) {
                    Bind b = f.getAnnotation(Bind.class);
                    bindTargets.add(new BindTarget(b.value(), b.after(), lookup.unreflectSetter(f), f.getType(), false));
                }
                if (f.isAnnotationPresent(OnClause.class)) {
                    if (f.getType() != boolean.class && f.getType() != Boolean.class) {
                        throw new DslDefinitionException(
                                "@OnClause on field '" + f.getName() + "' must be of type boolean or java.lang.Boolean."
                        );
                    }
                    clauseTargets.add(new OnClauseTarget(
                            f.getAnnotation(OnClause.class).value(),
                            lookup.unreflectSetter(f),
                            f.getType(),
                            false
                    ));
                }
            }

            for (Method m : clazz.getDeclaredMethods()) {
                if (m.isAnnotationPresent(Bind.class)) {
                    if (m.getParameterCount() != 1) {
                        throw new DslDefinitionException("@Bind method requires exactly 1 parameter.");
                    }
                    Bind b = m.getAnnotation(Bind.class);
                    bindTargets.add(new BindTarget(b.value(), b.after(), lookup.unreflect(m), m.getParameterTypes()[0], true));
                }
                if (m.isAnnotationPresent(OnClause.class)) {
                    if (m.getParameterCount() != 0) {
                        throw new DslDefinitionException("@OnClause method requires 0 parameters.");
                    }
                    OnClause o = m.getAnnotation(OnClause.class);
                    clauseTargets.add(new OnClauseTarget(o.value(), lookup.unreflect(m), void.class, true));
                }
            }

            return new TargetCache(bindTargets, clauseTargets);
        } catch (DslDefinitionException e) {
            throw e;
        } catch (Exception e) {
            throw new DslDefinitionException("Handle resolution failed", e);
        }
    }

    private void registerDefaultConverters() {
        registerConverter(String.class, v -> v);
        registerConverter(Integer.class, Integer::valueOf);
        registerConverter(int.class, Integer::parseInt);
        registerConverter(Boolean.class, Boolean::valueOf);
        registerConverter(boolean.class, Boolean::parseBoolean);
        registerConverter(Long.class, Long::valueOf);
        registerConverter(long.class, Long::parseLong);
    }
}