BeanWrapper.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.beans;

import io.micronaut.core.annotation.AnnotationMetadataProvider;
import io.micronaut.core.beans.exceptions.IntrospectionException;
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.convert.ConversionContext;
import io.micronaut.core.convert.ConversionError;
import io.micronaut.core.convert.exceptions.ConversionErrorException;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.ArgumentUtils;

import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import java.util.Collection;
import java.util.Optional;

/**
 * Wraps a bean allowing to read and write its properties via an underlying {@link BeanIntrospection}.
 *
 * @param <T> The bean type.
 * @author graemerocher
 * @since 1.1
 */
public interface BeanWrapper<T> extends AnnotationMetadataProvider {

    /**
     * @return The introspection.
     */
    @NonNull BeanIntrospection<T> getIntrospection();

    /**
     * @return The bean;
     */
    @NonNull T getBean();

    /**
     * The property names.
     *
     * @return The property names
     */
    default @NonNull String[] getPropertyNames() {
        return getIntrospection().getPropertyNames();
    }

    /**
     * The properties.
     * @return The properties
     */
    default @NonNull Collection<BeanProperty<T, Object>> getBeanProperties() {
        return getIntrospection().getBeanProperties();
    }

    /**
     * Get the property value of the given type or throw an exception if it is unobtainable.
     *
     * @param name The name
     * @param type The type
     * @param <P> The property generic type
     * @return The property value
     * @throws IntrospectionException if no property exists
     * @throws ConversionErrorException if the property cannot be converted to the given type
     */
    default @NonNull <P> P getRequiredProperty(@NonNull String name, @NonNull Class<P> type) {
        ArgumentUtils.requireNonNull("name", name);
        ArgumentUtils.requireNonNull("type", type);
        final Argument<P> argument = Argument.of(type);
        return getRequiredProperty(name, argument);
    }

    /**
     * Get the property value of the given type or throw an exception if it is unobtainable.
     *
     * @param name The name
     * @param argument The argument type
     * @param <P> The property generic type
     * @return The property value
     * @throws IntrospectionException if no property exists
     * @throws ConversionErrorException if the property cannot be converted to the given type
     */
    default <P> P getRequiredProperty(@NonNull String name, @NonNull Argument<P> argument) {
        ArgumentUtils.requireNonNull("name", name);
        ArgumentUtils.requireNonNull("argument", argument);

        final ArgumentConversionContext<P> context = ConversionContext.of(argument);
        return getRequiredProperty(name, context);
    }

    /**
     * Get the property value of the given type or throw an exception if it is unobtainable.
     * @param name The name
     * @param context The type
     * @param <P> The property generic type
     * @return The property value
     * @throws IntrospectionException if no property exists
     * @throws ConversionErrorException if the property cannot be converted to the given type
     */
    default @NonNull <P> P getRequiredProperty(@NonNull String name, @NonNull ArgumentConversionContext<P> context) {
        ArgumentUtils.requireNonNull("name", name);
        ArgumentUtils.requireNonNull("type", context);

        return getIntrospection().getProperty(name)
                .map(prop -> {
                    final Optional<P> converted = prop.get(getBean(), context);
                    return converted.orElseThrow(() -> {
                        final ConversionError conversionError = context.getLastError().orElseGet(() -> new ConversionError() {
                            @Override
                            public Exception getCause() {
                                return new IntrospectionException("Property of type [" + prop.getType() + "] cannot be converted to type: " + context.getArgument().getType());
                            }

                            @Override
                            public Optional<Object> getOriginalValue() {
                                return Optional.ofNullable(prop.get(getBean()));
                            }
                        });

                        return new ConversionErrorException(context.getArgument(), conversionError);
                    });
                })
                .orElseThrow(() -> new IntrospectionException("No property found for name: " + name));
    }

    /**
     * Get the property of the given name and type.
     * @param name The name
     * @param type The required type
     * @param <P> The required generic type
     * @return The property if found and can be converted to the given type
     */
    default @NonNull <P> Optional<P> getProperty(@NonNull String name, @NonNull Class<P> type) {
        ArgumentUtils.requireNonNull("name", name);
        ArgumentUtils.requireNonNull("type", type);
        final Argument<P> argument = Argument.of(type);
        return getProperty(name, argument);
    }

    /**
     * Get the property of the given name and type.
     * @param name The name
     * @param type The required type
     * @param <P> The required generic type
     * @return The property if found and can be converted to the given type
     */
    default <P> Optional<P> getProperty(@NonNull String name, Argument<P> type) {
        ArgumentUtils.requireNonNull("name", name);
        ArgumentUtils.requireNonNull("type", type);
        final ArgumentConversionContext<P> context = ConversionContext.of(type);
        return getProperty(name, context);
    }

    /**
     * Get the property of the given name and type.
     * @param name The name
     * @param context The conversion context
     * @param <P> The required generic type
     * @return The property if found and can be converted to the given type
     */
    default <P> Optional<P> getProperty(@NonNull String name, ArgumentConversionContext<P> context) {
        ArgumentUtils.requireNonNull("name", name);
        ArgumentUtils.requireNonNull("context", context);

        return getIntrospection().getProperty(name)
                                 .flatMap(prop -> prop.get(getBean(), context));
    }

    /**
     * Sets a property of the given name to the given value.
     * @param name The name
     * @param value The value
     * @return This wrapper
     * @throws ConversionErrorException if the value cannot be converted to the underlying property type.
     */
    default BeanWrapper<T> setProperty(@NonNull String name, @Nullable Object value) {
        ArgumentUtils.requireNonNull("name", name);

        getIntrospection()
                .getProperty(name)
                .ifPresent(prop -> prop.convertAndSet(getBean(), value));
        return this;
    }

    /**
     * Obtain a bean wrapper for the given bean.
     * @param bean The bean
     * @param <T2> The bean type
     * @return The wrapper
     * @throws io.micronaut.core.beans.exceptions.IntrospectionException If the wrapper cannot be created
     */
    static @NonNull <T2>  BeanWrapper<T2> getWrapper(@NonNull T2 bean) {
        ArgumentUtils.requireNonNull("bean", bean);
        @SuppressWarnings("unchecked") final Class<T2> aClass = (Class<T2>) bean.getClass();
        final BeanIntrospection<T2> introspection = BeanIntrospection.getIntrospection(aClass);
        return new DefaultBeanWrapper<>(bean, introspection);
    }


    /**
     * Obtain a bean wrapper for the given bean.
     * @param bean The bean
     * @param <T2> The bean type
     * @return The wrapper
     * @throws io.micronaut.core.beans.exceptions.IntrospectionException If the wrapper cannot be created
     */
    @SuppressWarnings("unchecked")
    static @NonNull <T2>  Optional<BeanWrapper<T2>> findWrapper(@NonNull T2 bean) {
        ArgumentUtils.requireNonNull("bean", bean);
        @SuppressWarnings("unchecked") final Class<T2> aClass = (Class<T2>) bean.getClass();
        return BeanIntrospector.SHARED.findIntrospection(aClass).map(i ->
                new DefaultBeanWrapper(bean, i)
        );
    }

    /**
     * Obtain a bean wrapper for the given bean.
     * @param type the type
     * @param bean The bean
     * @param <T2> The bean type
     * @return The wrapper
     * @throws io.micronaut.core.beans.exceptions.IntrospectionException If the wrapper cannot be created
     */
    @SuppressWarnings("unchecked")
    static @NonNull <T2>  Optional<BeanWrapper<T2>> findWrapper(Class<T2> type, @NonNull T2 bean) {
        ArgumentUtils.requireNonNull("type", type);
        ArgumentUtils.requireNonNull("bean", bean);
        return BeanIntrospector.SHARED.findIntrospection(type).map(i ->
                new DefaultBeanWrapper(bean, i)
        );
    }

    /**
     * Obtain a bean wrapper for the given bean.
     * @param type the type
     * @param bean The bean
     * @param <T2> The bean type
     * @return The wrapper
     * @throws io.micronaut.core.beans.exceptions.IntrospectionException If the wrapper cannot be created
     */
    static @NonNull <T2>  BeanWrapper<T2> getWrapper(Class<T2> type, @NonNull T2 bean) {
        ArgumentUtils.requireNonNull("type", type);
        ArgumentUtils.requireNonNull("bean", bean);
        final BeanIntrospection<T2> introspection = BeanIntrospection.getIntrospection(type);
        return new DefaultBeanWrapper<>(bean, introspection);
    }
}