/*
* 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);
}
}