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

org.apache.bval.jsr.job.ValidateProperty Maven / Gradle / Ivy

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.bval.jsr.job;

import java.lang.reflect.Array;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;

import jakarta.validation.ConstraintViolation;
import jakarta.validation.ValidationException;
import jakarta.validation.metadata.BeanDescriptor;
import jakarta.validation.metadata.CascadableDescriptor;
import jakarta.validation.metadata.ContainerDescriptor;
import jakarta.validation.metadata.ContainerElementTypeDescriptor;
import jakarta.validation.metadata.ElementDescriptor;
import jakarta.validation.metadata.PropertyDescriptor;
import jakarta.validation.valueextraction.ValueExtractor;
import jakarta.validation.valueextraction.ValueExtractor.ValueReceiver;

import org.apache.bval.jsr.ApacheFactoryContext;
import org.apache.bval.jsr.ConstraintViolationImpl;
import org.apache.bval.jsr.GraphContext;
import org.apache.bval.jsr.descriptor.BeanD;
import org.apache.bval.jsr.descriptor.CascadableContainerD;
import org.apache.bval.jsr.descriptor.ComposedD;
import org.apache.bval.jsr.descriptor.ConstraintD;
import org.apache.bval.jsr.descriptor.ContainerElementTypeD;
import org.apache.bval.jsr.descriptor.DescriptorManager;
import org.apache.bval.jsr.descriptor.ElementD;
import org.apache.bval.jsr.descriptor.PropertyD;
import org.apache.bval.jsr.groups.GroupStrategy;
import org.apache.bval.jsr.metadata.ContainerElementKey;
import org.apache.bval.jsr.util.PathImpl;
import org.apache.bval.jsr.util.PathNavigation;
import org.apache.bval.util.Exceptions;
import org.apache.bval.util.ObjectWrapper;
import org.apache.bval.util.StringUtils;
import org.apache.bval.util.Validate;
import org.apache.bval.util.reflection.TypeUtils;

public final class ValidateProperty extends ValidationJob {

    interface Strategy {
        T getRootBean();

        PathNavigation.Callback callback(PathImpl.Builder pathBuilder, FindDescriptor findDescriptor,
            ObjectWrapper reachable);

        ValidateProperty.Frame frame(ValidateProperty job, PathImpl path);
    }

    static class ForBeanProperty implements Strategy {
        final ApacheFactoryContext validatorContext;
        final T rootBean;
        final GraphContext rootContext;
        final ObjectWrapper leafContext;
        final ObjectWrapper value;

        ForBeanProperty(ApacheFactoryContext validatorContext, T bean) {
            super();
            this.validatorContext = validatorContext;
            this.rootBean = bean;
            this.rootContext = new GraphContext(validatorContext, PathImpl.create(), bean);
            this.leafContext = new ObjectWrapper<>(rootContext);
            this.value = new ObjectWrapper<>(bean);
        }

        @Override
        public T getRootBean() {
            return rootBean;
        }

        @Override
        public PathNavigation.Callback callback(PathImpl.Builder pathBuilder, FindDescriptor findDescriptor,
            ObjectWrapper reachable) {
            return new WalkGraph(validatorContext, rootBean.getClass(), pathBuilder, findDescriptor, value, reachable,
                (p, v) -> leafContext.accept(p.isRootPath() ? rootContext : rootContext.child(p, v)));
        }

        @Override
        public ValidateProperty.Frame frame(ValidateProperty job, PathImpl path) {
            if (job.descriptor instanceof BeanDescriptor) {
                return job.new LeafFrame<>(leafContext.get());
            }
            return job.new PropertyFrame>(job.new BeanFrame<>(leafContext.get()),
                (PropertyD) job.descriptor, leafContext.get().child(path, value.get()));
        }
    }

    static class ForPropertyValue implements Strategy {
        final ApacheFactoryContext validatorContext;
        final Class rootBeanClass;
        final Object value;

        ForPropertyValue(ApacheFactoryContext validatorContext, Class rootBeanClass, Object value) {
            super();
            this.validatorContext = validatorContext;
            this.rootBeanClass = rootBeanClass;
            this.value = value;
        }

        @Override
        public T getRootBean() {
            return null;
        }

        @Override
        public PathNavigation.Callback callback(PathImpl.Builder pathBuilder, FindDescriptor findDescriptor,
            ObjectWrapper reachable) {
            return new WalkGraph(validatorContext, rootBeanClass, pathBuilder, findDescriptor, new ObjectWrapper<>(),
                reachable, null);
        }

        @Override
        public ValidateProperty.Frame frame(ValidateProperty job, PathImpl path) {
            final GraphContext context = new GraphContext(job.validatorContext, path, value);
            if (job.descriptor instanceof BeanDescriptor) {
                return job.new LeafFrame<>(context);
            }
            return job.new PropertyFrame>(null, (PropertyD) job.descriptor, context);
        }
    }

    private interface Step {
        Type type();

        ElementD element();
    }

    private static class DescriptorWrapper implements Step {
        final ElementD wrapped;

        DescriptorWrapper(ElementDescriptor wrapped) {
            super();
            this.wrapped = (ElementD) wrapped;
        }

        @Override
        public Type type() {
            return wrapped.getGenericType();
        }

        @Override
        public ElementD element() {
            return wrapped;
        }
    }

    private static class TypeWrapper implements Step {
        final ApacheFactoryContext validatorContext;
        final Type type;

        TypeWrapper(ApacheFactoryContext validatorContext, Type type) {
            super();
            this.validatorContext = validatorContext;
            this.type = type;
        }

        @Override
        public Type type() {
            return type;
        }

        @Override
        public ElementD element() {
            final Class beanClass = TypeUtils.getRawType(type, null);
            return beanClass == null ? null
                : (BeanD) validatorContext.getDescriptorManager().getBeanDescriptor(beanClass);
        }
    }

    private static class FindDescriptor implements PathNavigation.Callback> {
        private final ApacheFactoryContext validatorContext;
        Step current;

        FindDescriptor(ApacheFactoryContext validatorContext, Class beanClass) {
            this.validatorContext = validatorContext;
            this.current = new DescriptorWrapper(validatorContext.getDescriptorManager().getBeanDescriptor(beanClass));
        }

        @Override
        public void handleProperty(String name) {
            final ElementDescriptor element = current.element();
            final BeanD bean;
            if (element instanceof BeanD) {
                bean = (BeanD) element;
            } else {
                bean = (BeanD) validatorContext.getDescriptorManager().getBeanDescriptor(element.getElementClass());
            }
            final PropertyDescriptor property = bean.getProperty(name);
            if (property == null) {
                Exceptions.raise(IllegalArgumentException::new, "Unknown property %s of %s", name,
                    bean.getElementClass());
            }
            current = new DescriptorWrapper(property);
        }

        @Override
        public void handleIndexOrKey(String value) {
            handleGenericInIterable();
        }

        @Override
        public void handleGenericInIterable() {
            final ElementDescriptor desc = current.element();
            if (desc instanceof CascadableContainerD) {
                final Step containerElement = handleContainerElement((CascadableContainerD) desc);
                if (containerElement != null) {
                    current = containerElement;
                    return;
                }
            }
            current = handleElementByType(current.type());
        }

        private Step handleContainerElement(CascadableContainerD desc) {
            final Set containerElements = desc.getConstrainedContainerElementTypes();
            if (containerElements.isEmpty()) {
                return null;
            }
            final ContainerElementTypeDescriptor element;
            if (containerElements.size() == 1) {
                element = containerElements.iterator().next();
            } else {
                final Collection> wellKnown = Arrays.asList(MAP_VALUE, ITERABLE_ELEMENT);

                final Optional found =
                    containerElements.stream(). map(ContainerElementTypeD.class::cast)
                        .filter(d -> wellKnown.stream().anyMatch(d.getKey()::represents)).findFirst();

                if (!found.isPresent()) {
                    return null;
                }
                element = found.get();
            }
            return new DescriptorWrapper(element);
        }

        private Step handleElementByType(Type type) {
            Type elementType;

            if (TypeUtils.isArrayType(type)) {
                elementType = TypeUtils.getArrayComponentType(type);
            } else if (TypeUtils.isAssignable(type, Map.class)) {
                elementType =
                    Optional.ofNullable(TypeUtils.getTypeArguments(type, Map.class).get(MAP_VALUE)).orElse(MAP_VALUE);
            } else if (TypeUtils.isAssignable(type, Iterable.class)) {
                elementType =
                    Optional.ofNullable(TypeUtils.getTypeArguments(type, Iterable.class).get(ITERABLE_ELEMENT))
                        .orElse(ITERABLE_ELEMENT);
            } else {
                throw Exceptions.create(IllegalArgumentException::new, "Unable to resolve element type of %s", type);
            }
            return new TypeWrapper(validatorContext, elementType);
        }

        @Override
        public ElementD result() {
            return current.element();
        }
    }

    private static class WalkGraph extends PathNavigation.CallbackProcedure {
        final ApacheFactoryContext validatorContext;
        final Class rootBeanClass;
        final PathImpl.Builder pathBuilder;
        final FindDescriptor findDescriptor;
        final ObjectWrapper value;
        final ObjectWrapper reachable;
        final BiConsumer recordLeaf;

        WalkGraph(ApacheFactoryContext validatorContext, Class rootBeanClass, PathImpl.Builder pathBuilder,
            FindDescriptor findDescriptor, ObjectWrapper value, ObjectWrapper reachable,
            BiConsumer recordLeaf) {
            this.validatorContext = validatorContext;
            this.rootBeanClass = rootBeanClass;
            this.pathBuilder = pathBuilder;
            this.findDescriptor = findDescriptor;
            this.value = value;
            this.reachable = reachable;
            this.recordLeaf = recordLeaf;
        }

        @Override
        public void handleProperty(String name) {
            final PathImpl p = PathImpl.copy(pathBuilder.result());
            pathBuilder.handleProperty(name);
            findDescriptor.handleProperty(name);

            if (reachable.get().booleanValue()) {
                try {
                    reachable.accept(validatorContext.getTraversableResolver().isReachable(value.get(),
                        pathBuilder.result().getLeafNode(), rootBeanClass, p,
                        findDescriptor.result().getElementType()));
                } catch (ValidationException ve) {
                    throw ve;
                } catch (Exception e) {
                    throw new ValidationException(e);
                }
            }
            if (reachable.get().booleanValue() && value.optional().isPresent() && recordLeaf != null) {
                recordLeaf.accept(p, value.get());

                final PropertyD propertyD =
                    ComposedD.unwrap(findDescriptor.current.element(), PropertyD.class).findFirst().get();
                try {
                    value.accept(propertyD.getValue(value.get()));
                } catch (Exception e) {
                    Exceptions.raise(IllegalStateException::new, e, "Unable to get value of property %s",
                        propertyD.getPropertyName());
                }
            }
        }

        @Override
        public void handleIndexOrKey(final String indexOrKey) {
            pathBuilder.handleIndexOrKey(indexOrKey);
            findDescriptor.handleIndexOrKey(indexOrKey);
            if (value.optional().isPresent()) {
                ElementDescriptor element = findDescriptor.current.element();
                if (element instanceof ContainerElementTypeD) {
                    value.accept(handleContainer(value.get(), ((ContainerElementTypeD) element).getKey(), indexOrKey));
                } else {
                    value.accept(handleBasic(value.get(), indexOrKey));

                    if (element == null && value.optional().isPresent()) {
                        // no generic info available at some previous index level; fall back to runtime type of value
                        // and repair structure of findDescriptor:
                        findDescriptor.current = new TypeWrapper(validatorContext, value.get().getClass());
                        element = findDescriptor.current.element();
                    }
                    if (element instanceof BeanDescriptor) {
                        recordLeaf.accept(PathImpl.copy(pathBuilder.result()), value.get());
                    }
                }
            }
        }

        @SuppressWarnings("unchecked")
        private Object handleContainer(Object o, ContainerElementKey key, String indexOrKey) {
            @SuppressWarnings("rawtypes")
            final ValueExtractor valueExtractor = validatorContext.getValueExtractors().find(key);

            final ObjectWrapper result = new ObjectWrapper<>();
            valueExtractor.extractValues(o, new ValueReceiver() {

                @Override
                public void indexedValue(String nodeName, int index, Object object) {
                    if (Integer.toString(index).equals(indexOrKey)) {
                        result.accept(object);
                    }
                }

                @Override
                public void iterableValue(String nodeName, Object object) {
                    // ?
                    result.accept(object);
                }

                @Override
                public void keyedValue(String nodeName, Object key, Object object) {
                    if (String.valueOf(key).equals(indexOrKey)) {
                        result.accept(object);
                    }
                }

                @Override
                public void value(String nodeName, Object object) {
                    // ?
                    result.accept(object);
                }
            });
            return result.get();
        }

        private Object handleBasic(Object o, String indexOrKey) {
            if (Map.class.isInstance(o)) {
                for (Map.Entry e : ((Map) o).entrySet()) {
                    if (String.valueOf(e.getKey()).equals(indexOrKey)) {
                        return e.getValue();
                    }
                }
            } else {
                try {
                    final int index = Integer.parseInt(indexOrKey);
                    if (index < 0) {
                        Exceptions.raise(IllegalArgumentException::new, "Invalid index %d", index);
                    }
                    if (o != null && TypeUtils.isArrayType(o.getClass())) {
                        if (Array.getLength(o) > index) {
                            return Array.get(o, index);
                        }
                    } else if (List.class.isInstance(o)) {
                        final List l = (List) o;
                        if (l.size() > index) {
                            return l.get(index);
                        }
                    } else if (Iterable.class.isInstance(o)) {
                        int i = -1;
                        for (Object e : (Iterable) o) {
                            if (++i == index) {
                                return e;
                            }
                        }
                    }
                } catch (NumberFormatException e) {
                }
            }
            return null;
        }

        @Override
        public void handleGenericInIterable() {
            throw new UnsupportedOperationException("Cannot resolve generic inIterable against actual object graph");
        }
    }

    class LeafFrame extends BeanFrame {

        LeafFrame(GraphContext context) {
            super(context);
        }

        @Override
        protected ValidationJob.Frame propertyFrame(PropertyD d, GraphContext context) {
            return new PropertyFrame<>(this, d, context);
        }
    }

    class PropertyFrame & CascadableDescriptor & ContainerDescriptor> extends SproutFrame {

        PropertyFrame(ValidationJob.Frame parent, D descriptor, GraphContext context) {
            super(parent, descriptor, context);
        }

        @Override
        void recurse(GroupStrategy groups, Consumer> sink) {
            if (cascade) {
                super.recurse(groups, sink);
            }
        }
    }

    private final Strategy strategy;
    private final Class rootBeanClass;
    private final PathImpl propertyPath;
    private final T rootBean;
    private final boolean reachable;
    private ElementD descriptor;
    private boolean cascade;

    private ValidateProperty(Strategy strategy, ApacheFactoryContext validatorContext, Class rootBeanClass,
        String property, Class[] groups) {
        super(validatorContext, groups);

        Exceptions.raiseIf(StringUtils.isBlank(property), IllegalArgumentException::new,
            "property cannot be null/empty/blank");

        this.strategy = strategy;
        this.rootBeanClass = Validate.notNull(rootBeanClass, IllegalArgumentException::new, "rootBeanClass");

        final PathImpl.Builder pathBuilder = new PathImpl.Builder();
        final FindDescriptor findDescriptor = new FindDescriptor(validatorContext, rootBeanClass);
        final ObjectWrapper reachable = new ObjectWrapper<>(Boolean.TRUE);

        PathNavigation.navigate(property, strategy.callback(pathBuilder, findDescriptor, reachable));

        this.propertyPath = pathBuilder.result();
        this.descriptor = findDescriptor.result();
        this.rootBean = strategy.getRootBean();
        this.reachable = reachable.get().booleanValue();
    }

    ValidateProperty(ApacheFactoryContext validatorContext, Class rootBeanClass, String property, Object value,
        Class[] groups) {
        this(new ForPropertyValue<>(validatorContext, rootBeanClass, value), validatorContext, rootBeanClass, property,
            groups);
        if (descriptor == null) {
            // should only occur when the root class is raw

            final Class t;
            if (value == null) {
                t = Object.class;
            } else {
                t = value.getClass();
            }
            descriptor = (ElementD) validatorContext.getDescriptorManager().getBeanDescriptor(t);
        } else if (hasWork()) {
            final Class propertyType = descriptor.getElementClass();
            if (!TypeUtils.isInstance(value, propertyType)) {
                Exceptions.raise(IllegalArgumentException::new, "%s is not an instance of %s", value, propertyType);
            }
        }
    }

    @SuppressWarnings("unchecked")
    ValidateProperty(ApacheFactoryContext validatorContext, T bean, String property, Class[] groups)
        throws Exception {
        this(new ForBeanProperty<>(validatorContext, Validate.notNull(bean, IllegalArgumentException::new, "bean")),
            validatorContext, (Class) bean.getClass(), property, groups);

        if (descriptor == null) {
            Exceptions.raise(IllegalArgumentException::new, "Could not resolve property name/path: %s", property);
        }
    }

    public ValidateProperty cascade(boolean cascade) {
        this.cascade = cascade;
        return this;
    }

    @Override
    protected Frame computeBaseFrame() {
        // TODO assign bean as its own property and figure out what to do
        return strategy.frame(this, propertyPath);
    }

    @Override
    protected Class getRootBeanClass() {
        return rootBeanClass;
    }

    @Override
    protected boolean hasWork() {
        if (!reachable) {
            return false;
        }
        if (descriptor instanceof BeanDescriptor) {
            return ((BeanDescriptor) descriptor).isBeanConstrained();
        }
        return DescriptorManager.isConstrained((PropertyDescriptor) descriptor);
    }

    @Override
    ConstraintViolationImpl createViolation(String messageTemplate, String message,
        ConstraintValidatorContextImpl context, PathImpl propertyPath) {
        return new ConstraintViolationImpl<>(messageTemplate, message, rootBean, context.getFrame().getBean(),
            propertyPath, context.getFrame().context.getValue(), context.getConstraintDescriptor(), rootBeanClass,
            context.getConstraintDescriptor().unwrap(ConstraintD.class).getDeclaredOn(), null, null);
    }
}