AbstractArgumentBinder.java

/*
 * Copyright 2017-2020 original authors
 *
 * 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 io.micronaut.core.bind.annotation;

import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.bind.ArgumentBinder.BindingResult;
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.convert.ConversionError;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.convert.value.ConvertibleValues;
import io.micronaut.core.naming.NameUtils;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.StringUtils;

import java.util.List;
import java.util.Optional;

/**
 * An abstract {@link AnnotatedArgumentBinder} implementation.
 *
 * @param <T> The argument type
 * @author Graeme Rocher
 * @since 1.0
 */
public abstract class AbstractArgumentBinder<T> {

    private static final String DEFAULT_VALUE_MEMBER = "defaultValue";
    protected final ConversionService conversionService;

    private final String parameterName;
    private final String fallbackParameterName;
    private final String defaultValue;

    /**
     * Constructor.
     *
     * @param conversionService conversionService
     */
    protected AbstractArgumentBinder(ConversionService conversionService) {
        this.conversionService = conversionService;
        this.parameterName = null;
        this.fallbackParameterName = null;
        this.defaultValue = null;
    }

    /**
     * Constructor.
     *
     * @param conversionService conversionService
     * @param argument The argument
     */
    protected AbstractArgumentBinder(ConversionService conversionService, Argument<T> argument) {
        this.conversionService = conversionService;
        this.parameterName = getParameterName(argument);
        this.fallbackParameterName = getFallbackFormat(argument);
        this.defaultValue = argument.getAnnotationMetadata().stringValue(Bindable.class, DEFAULT_VALUE_MEMBER).orElse(null);
    }

    /**
     * Do binding.
     *
     * @param context context
     * @param values  values
     * @param name    name
     * @return result
     */
    protected BindingResult<T> doBind(
        ArgumentConversionContext<T> context,
        ConvertibleValues<?> values,
        String name) {
        return doBind(context, values, name, BindingResult.empty());
    }

    /**
     * Do binding.
     *
     * @param context context
     * @param values  values
     * @return result
     */
    protected BindingResult<T> doBind(
        ArgumentConversionContext<T> context,
        ConvertibleValues<?> values) {
        return doBind(context, values, resolvedParameterName(context.getArgument()), BindingResult.empty());
    }

    /**
     * Do binding.
     *
     * @param context       context
     * @param values        values
     * @param name          annotationValue
     * @param defaultResult The default binding result if the value is null
     * @return result
     */
    protected BindingResult<T> doBind(ArgumentConversionContext<T> context,
                                      ConvertibleValues<?> values,
                                      String name,
                                      BindingResult<T> defaultResult) {

        return doConvert(doResolve(context, values, name), context, defaultResult);
    }

    /**
     * Do binding.
     *
     * @param context       context
     * @param values        values
     * @param defaultResult The default binding result if the value is null
     * @return result
     * @since 4.8
     */
    protected BindingResult<T> doBind(ArgumentConversionContext<T> context,
                                      ConvertibleValues<?> values,
                                      BindingResult<T> defaultResult) {

        return doBind(context, values, resolvedParameterName(context.getArgument()), defaultResult);
    }

    /**
     * Find the parameter name.
     *
     * @param argument The argument
     * @return The name
     * @since 4.8
     */
    @NonNull
    protected String getParameterName(@NonNull Argument<T> argument) {
        throw new IllegalStateException("Parameter resolved method must be implemented!");
    }

    /**
     * Returns resolved parameter name using {@link #getParameterName(Argument)} or pre-resolved.
     * @param argument The argument.
     * @return The parameter name
     * @since 4.8
     */
    protected final String resolvedParameterName(Argument<T> argument) {
        if (parameterName == null) {
            return getParameterName(argument);
        }
        return parameterName;
    }

    /**
     * Do resolve.
     *
     * @param context context
     * @param values  values
     * @param name    annotationValue
     * @return result
     */
    @Nullable
    protected Object doResolve(ArgumentConversionContext<T> context,
                               ConvertibleValues<?> values,
                               String name) {

        Object value = resolveValue(context, values, name);
        if (value == null) {
            String fallbackName = getFallbackFormatInternal(context.getArgument());
            if (!name.equals(fallbackName)) {
                name = fallbackName;
                value = resolveValue(context, values, name);
            }
        }

        return value;
    }

    /**
     * Do resolve.
     *
     * @param context context
     * @param values  values
     * @return result
     * @since 4.8
     */
    @Nullable
    protected Object doResolve(ArgumentConversionContext<T> context,
                               ConvertibleValues<?> values) {
        String name = resolvedParameterName(context.getArgument());
        return doResolve(context, values, name);
    }

    /**
     * @param argument The argument
     * @return The fallback format
     */
    private String getFallbackFormatInternal(Argument<?> argument) {
        if (fallbackParameterName == null) {
            return NameUtils.hyphenate(argument.getName());
        }
        return fallbackParameterName;
    }

    /**
     * @param argument The argument
     * @return The fallback format
     */
    protected String getFallbackFormat(Argument<?> argument) {
        return NameUtils.hyphenate(argument.getName());
    }

    private Object resolveValue(ArgumentConversionContext<T> context, ConvertibleValues<?> values, String annotationValue) {
        Argument<T> argument = context.getArgument();
        if (StringUtils.isEmpty(annotationValue)) {
            annotationValue = argument.getName();
        }
        return values.get(annotationValue, context).orElseGet(() ->
            conversionService.convert(
                resolveDefaultValue(argument),
                context
            ).orElse(null)
        );
    }

    private String resolveDefaultValue(Argument<T> argument) {
        if (defaultValue == null) {
            return argument.getAnnotationMetadata().stringValue(Bindable.class, DEFAULT_VALUE_MEMBER).orElse(null);
        }
        return defaultValue;
    }

    /**
     * Convert the value and return a binding result.
     *
     * @param value   The value to convert
     * @param context The conversion context
     * @return The binding result
     */
    protected BindingResult<T> doConvert(Object value, ArgumentConversionContext<T> context) {
        return doConvert(value, context, BindingResult.empty());
    }

    /**
     * Convert the value and return a binding result.
     *
     * @param value         The value to convert
     * @param context       The conversion context
     * @param defaultResult The binding result if the value is null
     * @return The binding result
     */
    protected BindingResult<T> doConvert(Object value, ArgumentConversionContext<T> context, BindingResult<T> defaultResult) {
        if (value == null) {
            Optional<ConversionError> lastError = context.getLastError();
            if (lastError.isPresent()) {
                return new BindingResult<>() {
                    @Override
                    public Optional<T> getValue() {
                        return Optional.empty();
                    }

                    @Override
                    public List<ConversionError> getConversionErrors() {
                        return lastError.map(List::of).orElseGet(List::of);
                    }
                };
            }
            return defaultResult;
        } else {
            Optional<T> result = conversionService.convert(value, context);
            Optional<ConversionError> lastError = context.getLastError();
            if (result.isPresent() && context.getArgument().getType() == Optional.class) {
                result = (Optional<T>) result.get();
            }
            Optional<T> finalResult = result;
            return new BindingResult<>() {
                @Override
                public Optional<T> getValue() {
                    return finalResult;
                }

                @Override
                public List<ConversionError> getConversionErrors() {
                    return lastError.map(List::of).orElseGet(List::of);
                }
            };
        }
    }
}