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

org.algorithmx.rules.model.ParameterDefinition Maven / Gradle / Ivy

There is a newer version: 0.79.50
Show newest version
/**
 * This software is licensed under the Apache 2 license, quoted below.
 *
 * Copyright (c) 1999-2019, Live Software & Consultants Inc ([email protected])
 *
 * 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
 *
 * http://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 org.algorithmx.rules.model;

import org.algorithmx.rules.annotation.Description;
import org.algorithmx.rules.annotation.Nullable;
import org.algorithmx.rules.bind.Binding;
import org.algorithmx.rules.error.UnrulyException;
import org.algorithmx.rules.spring.util.Assert;
import org.algorithmx.rules.spring.util.ClassUtils;
import org.algorithmx.rules.util.ReflectionUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Optional;

/**
 * Defines a parameter within a method that is to be isPass dynamically (such as "when" and "then")
 *
 * The definition stores the parameter index, name of parameter (automatically discovered), generic type,
 * whether the parameter is required and any associated annotations.
 *
 * A parameter is deemed not required if it is annotated @Nullable or it is declared with an Optional type.
 *
 * @author Max Arulananthan
 * @since 1.0
 */
public final class ParameterDefinition {

    private static final SpecialParameter[] SPECIAL_PARAMETERS = new SpecialParameter[4];

    // Special Handling class handlers
    static {
        SPECIAL_PARAMETERS[0] = new ParameterizedBindingType();
        SPECIAL_PARAMETERS[1] = new ParameterizedOptionalType();
        SPECIAL_PARAMETERS[2] = new BindingType();
        SPECIAL_PARAMETERS[3] = new OptionalType();
    }

    private final int index;
    private final String name;
    private final String description;
    private final Type type;
    private final boolean required;
    private final Annotation[] annotations;
    private final SpecialParameter specialParameter;

    private ParameterDefinition(int index, String name, Type type, String description, boolean required, Annotation...annotations) {
        super();
        Assert.isTrue(index >= 0, "Parameter index must be >= 0");
        Assert.notNull(name, "Parameter name cannot be null");
        Assert.notNull(type, "Parameter type cannot be null");

        SpecialParameter specialParameter = findSpecialParameter(type);

        this.name = name;
        this.type = specialParameter != null ? specialParameter.deriveType(type) : type;
        this.description = description;
        this.index = index;
        this.annotations = annotations;
        this.required = specialParameter != null ? specialParameter.isRequired() : required;
        this.specialParameter = specialParameter;
    }

    /**
     * Loads the parameter definitions for the given method.
     *
     * @param method desired method
     * @return all the parameter definitions for the given method.
     */
    public static ParameterDefinition[] load(Method method) {
        String[] parameterNames = ReflectionUtils.getParameterNames(method);
        Assert.isTrue(parameterNames.length == method.getParameterTypes().length,
                "parameterNames length does not match parameter types length");
        ParameterDefinition[] result = new ParameterDefinition[method.getParameterTypes().length];

        for (int i = 0; i < method.getGenericParameterTypes().length; i++) {
            boolean required = isRequired(method, i);

            // Make sure the parameter isn't primitive
            if (!required && method.getParameters()[i].getType().isPrimitive()) {
                throw new UnrulyException("Primitive types cannot be optional. Method [" + method + "] Param ["
                        + parameterNames[i] + "] is declared Nullable however the type is primitive ["
                        + method.getParameters()[i].getType() + "] Use ["
                        + ClassUtils.getWrapperClass(method.getParameters()[i].getType()) + "] instead");
            }
            
            Description descriptionAnnotation = method.getParameters()[i].getAnnotation(Description.class);
            result[i] = new ParameterDefinition(i, parameterNames[i], method.getGenericParameterTypes()[i],
                    descriptionAnnotation != null ? descriptionAnnotation.value() : null, required,
                    method.getParameterAnnotations()[i]);
        }

        return result;
    }

    private static boolean isRequired(Method method, int index) {
        Nullable nullableAnnotation = method.getParameters()[index].getAnnotation(Nullable.class);
        // There is a Nullable annotation; It is optional.
        if (nullableAnnotation != null) return false;
        return method.getParameters()[index].getType().isPrimitive();
    }

    /**
     * See if the parameter needs special handling.
     *
     * @param type param type.
     * @return true if param requires special handling; false otherwise.
     */
    private SpecialParameter findSpecialParameter(Type type) {
        SpecialParameter result = null;

        for (SpecialParameter sp : SPECIAL_PARAMETERS) {
            if (sp.isApplicable(type)) {
                result = sp;
                break;
            }
        }

        return result;
    }

    /**
     * Returns the index of the parameter.
     *
     * @return Parameter index.
     */
    public int getIndex() {
        return index;
    }

    /**
     * Returns the name of the parameter.
     *
     * @return name of the parameter.
     */
    public String getName() {
        return name;
    }

    /**
     * Returns the Type of the parameter.
     *
     * @return type of the parameter.
     */
    public Type getType() {
        return type;
    }

    /**
     * Returns whether this parameter requires special handling (Binding, Optional etc).
     *
     * @return true if it requires special handling; false otherwise.
     */
    public boolean isSpecialParameter() {
        return specialParameter != null;
    }

    /**
     * Returns the special parameter if one is found.
     *
     * @return special parameter if one is found; null otherwise.
     */
    public SpecialParameter getSpecialParameter() {
        return specialParameter;
    }

    /**
     * Returns the parameter description.
     *
     * @return parameter description.
     */
    public String getDescription() {
        return description;
    }

    /**
     * Determines if this parameter is required.
     *
     * @return true if required; false otherwise.
     */
    public boolean isRequired() {
        return required;
    }

    /**
     * Returns true if this is a Binding.
     *
     * @return true if this is a Binding; false otherwise.
     */
    public boolean isBinding() {
        return specialParameter instanceof ParameterizedBindingType || specialParameter instanceof BindingType;
    }

    /**
     * Returns true if this is Optional.
     *
     * @return true if this is a Optional; false otherwise.
     */
    public boolean isOptional() {
        return specialParameter instanceof ParameterizedOptionalType || specialParameter instanceof OptionalType;
    }

    /**
     * Returns all the associated annotations for this parameter.
     *
     * @return all annotations for this parameter.
     */
    public Annotation[] getAnnotations() {
        return annotations;
    }

    @Override
    public String toString() {
        return "ParameterDefinition{" +
                "index=" + index +
                ", name='" + name + '\'' +
                ", type=" + type +
                ", specialParameter=" + specialParameter +
                ", required=" + required +
                ", annotations=" + Arrays.toString(annotations) +
                '}';
    }

    /**
     * Defines the properties of Parameter that has some special handling.
     *
     */
    public interface SpecialParameter {

        /**
         * Determines if the parameter is special parameter.
         *
         * @param param input parameter.
         * @return true if it is applicable; false otherwise.
         */
        boolean isApplicable(Object param);

        /**
         * Derive the bindable type.
         *
         * @param param input param.
         * @return derived param.
         */
        Type deriveType(Object param);

        /**
         * Returns if this parameter is required.
         * @return true if it is required; false otherwise.
         */
        boolean isRequired();

        /**
         * Retrive the value from the given Binding.
         *
         * @param binding input binding.
         * @param  Binding Type.
         * @return coerced value.
         */
         Object getValue(Binding binding);
    }

    /**
     * Binding with a parameterized type.
     */
    private static class ParameterizedBindingType implements SpecialParameter {

        public ParameterizedBindingType() {
            super();
        }

        @Override
        public boolean isApplicable(Object param) {
            return param instanceof ParameterizedType && Binding.class.equals(((ParameterizedType) param).getRawType());
        }

        @Override
        public Type deriveType(Object param) {
            return ((ParameterizedType) param).getActualTypeArguments()[0];
        }

        @Override
        public boolean isRequired() {
            return true;
        }

        @Override
        public  Object getValue(Binding binding) {
            return binding;
        }
    }

    /**
     * Optional with a parameterized type.
     */
    private static class ParameterizedOptionalType implements SpecialParameter {

        public ParameterizedOptionalType() {
            super();
        }

        @Override
        public boolean isApplicable(Object param) {
            return param instanceof ParameterizedType && Optional.class.equals(((ParameterizedType) param).getRawType());
        }

        @Override
        public Type deriveType(Object param) {
            return ((ParameterizedType) param).getActualTypeArguments()[0];
        }

        @Override
        public boolean isRequired() {
            return false;
        }

        @Override
        public  Object getValue(Binding binding) {
            Object result = binding.getValue();
            return result == null ? Optional.empty() : Optional.of(result);
        }
    }

    /**
     * Just the Binding class (ie: generic info not available). This usually happens with compiler does not store the
     * generic info for the lambda. JavaC!
     */
    private static class BindingType implements SpecialParameter {

        public BindingType() {
            super();
        }

        @Override
        public boolean isApplicable(Object param) {
            return Binding.class.equals(param);
        }

        @Override
        public Type deriveType(Object param) {
            return Object.class;
        }

        @Override
        public boolean isRequired() {
            return true;
        }

        @Override
        public  Object getValue(Binding binding) {
            return binding.get();
        }
    }

    /**
     * Just the Optional class (ie: generic info not available). This usually happens with compiler does not store the
     * generic info for the lambda. JavaC!
     */
    private static class OptionalType implements SpecialParameter {

        public OptionalType() {
            super();
        }

        @Override
        public boolean isApplicable(Object param) {
            return Optional.class.equals(param);
        }

        @Override
        public Type deriveType(Object param) {
            return Object.class;
        }

        @Override
        public boolean isRequired() {
            return false;
        }

        @Override
        public  Object getValue(Binding binding) {
            Object result = binding.getValue();
            return result == null ? Optional.empty() : Optional.of(result);
        }
    }
}