/*
* JBoss, Home of Professional Open Source.
* Copyright 2011, Red Hat Middleware LLC, and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.as.model.test;

import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ACCESS_CONSTRAINTS;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ACCESS_TYPE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ALLOWED;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ALTERNATIVES;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ATTRIBUTES;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.CHILDREN;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.DEFAULT;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.DEPRECATED;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.DESCRIPTION;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.EXPRESSIONS_ALLOWED;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.HEAD_COMMENT_ALLOWED;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.MAX;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.MAX_LENGTH;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.MAX_OCCURS;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.MIN;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.MIN_LENGTH;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.MIN_OCCURS;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.MODEL_DESCRIPTION;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.NAMESPACE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.NILLABLE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OPERATIONS;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OPERATION_NAME;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.REASON;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.REPLY_PROPERTIES;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.REQUEST_PROPERTIES;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.REQUIRED;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.REQUIRES;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.RESTART_REQUIRED;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SINCE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.STORAGE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.TAIL_COMMENT_ALLOWED;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.TYPE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.UNIT;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.VALUE_TYPE;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import org.jboss.as.controller.registry.AttributeAccess;
import org.jboss.as.controller.registry.AttributeAccess.AccessType;
import org.jboss.dmr.ModelNode;
import org.jboss.dmr.ModelType;

/**
 * Validates the types and entries of the of the description providers in the model, as read by
 * {@code /some/path:read-resource-description(recursive=true,inherited=false,operations=true)}
 *
 * @author <a href="kabir.khan@jboss.com">Kabir Khan</a>
 */
public class ModelTestModelDescriptionValidator {

    private static final Map<String, ArbitraryDescriptorValidator> VALID_RESOURCE_KEYS;
    private static final Map<String, ArbitraryDescriptorValidator> VALID_CHILD_TYPE_KEYS;
    private static final Map<String, AttributeOrParameterArbitraryDescriptorValidator> VALID_ATTRIBUTE_KEYS;
    private static final Map<String, ArbitraryDescriptorValidator> VALID_OPERATION_KEYS;
    private static final Map<String, AttributeOrParameterArbitraryDescriptorValidator> VALID_PARAMETER_KEYS;
    static {
        Map<String, ArbitraryDescriptorValidator> validResourceKeys = new HashMap<String, ArbitraryDescriptorValidator>();
        validResourceKeys.put(DESCRIPTION, NullDescriptorValidator.INSTANCE);
        validResourceKeys.put(HEAD_COMMENT_ALLOWED, BooleanDescriptorValidator.INSTANCE);
        validResourceKeys.put(TAIL_COMMENT_ALLOWED, BooleanDescriptorValidator.INSTANCE);
        validResourceKeys.put(NAMESPACE, NullDescriptorValidator.INSTANCE);
        validResourceKeys.put(ATTRIBUTES, NullDescriptorValidator.INSTANCE);
        validResourceKeys.put(OPERATIONS, NullDescriptorValidator.INSTANCE);
        validResourceKeys.put(CHILDREN, NullDescriptorValidator.INSTANCE);
        validResourceKeys.put(DEPRECATED, DeprecatedDescriptorValidator.INSTANCE);
        validResourceKeys.put(ACCESS_CONSTRAINTS, AccessConstraintValidator.INSTANCE);
        VALID_RESOURCE_KEYS = Collections.unmodifiableMap(validResourceKeys);

        Map<String, ArbitraryDescriptorValidator> validChildTypeKeys = new HashMap<String, ModelTestModelDescriptionValidator.ArbitraryDescriptorValidator>();
        validChildTypeKeys.put(DESCRIPTION, NullDescriptorValidator.INSTANCE);
        validChildTypeKeys.put(MODEL_DESCRIPTION, NullDescriptorValidator.INSTANCE);
        validChildTypeKeys.put(MIN_OCCURS, SimpleIntDescriptorValidator.INSTANCE);
        validChildTypeKeys.put(MAX_OCCURS, SimpleIntDescriptorValidator.INSTANCE);
        validChildTypeKeys.put(ALLOWED, StringListValidator.INSTANCE);
        VALID_CHILD_TYPE_KEYS = Collections.unmodifiableMap(validChildTypeKeys);


        Map<String, AttributeOrParameterArbitraryDescriptorValidator> paramAndAttributeKeys = new HashMap<String, AttributeOrParameterArbitraryDescriptorValidator>();
        //Normal
        paramAndAttributeKeys.put(DESCRIPTION, NullDescriptorValidator.INSTANCE);
        paramAndAttributeKeys.put(REQUIRED, BooleanDescriptorValidator.INSTANCE);
        paramAndAttributeKeys.put(NILLABLE, BooleanDescriptorValidator.INSTANCE);
        paramAndAttributeKeys.put(VALUE_TYPE, NullDescriptorValidator.INSTANCE);
        paramAndAttributeKeys.put(TYPE, NullDescriptorValidator.INSTANCE);
        //Arbitrary
        paramAndAttributeKeys.put(ALTERNATIVES, StringListValidator.INSTANCE);
        paramAndAttributeKeys.put(REQUIRES, StringListValidator.INSTANCE);
        paramAndAttributeKeys.put(MIN, NumericDescriptorValidator.INSTANCE);
        paramAndAttributeKeys.put(MAX, NumericDescriptorValidator.INSTANCE);
        paramAndAttributeKeys.put(MIN_LENGTH, LengthDescriptorValidator.INSTANCE);
        paramAndAttributeKeys.put(MAX_LENGTH, LengthDescriptorValidator.INSTANCE);
        paramAndAttributeKeys.put(DEFAULT, NullDescriptorValidator.INSTANCE); //TODO Validate same type as target type
        paramAndAttributeKeys.put(ALLOWED, StringListValidator.INSTANCE);
        paramAndAttributeKeys.put(UNIT, NumericDescriptorValidator.INSTANCE);
        paramAndAttributeKeys.put(EXPRESSIONS_ALLOWED, BooleanDescriptorValidator.INSTANCE);
        paramAndAttributeKeys.put(DEPRECATED, DeprecatedDescriptorValidator.INSTANCE);

        Map<String, AttributeOrParameterArbitraryDescriptorValidator> validAttributeKeys = new HashMap<String, AttributeOrParameterArbitraryDescriptorValidator>();
        validAttributeKeys.putAll(paramAndAttributeKeys);
        validAttributeKeys.put(ACCESS_TYPE, AccessTypeDescriptorValidator.INSTANCE);
        validAttributeKeys.put(HEAD_COMMENT_ALLOWED, BooleanDescriptorValidator.INSTANCE);
        validAttributeKeys.put(TAIL_COMMENT_ALLOWED, BooleanDescriptorValidator.INSTANCE);
        validAttributeKeys.put(STORAGE, StorageDescriptorValidator.INSTANCE);
        validAttributeKeys.put(RESTART_REQUIRED, NullDescriptorValidator.INSTANCE); //TODO some more validation here
        validAttributeKeys.put(DEPRECATED, DeprecatedDescriptorValidator.INSTANCE);
        validAttributeKeys.put(ACCESS_CONSTRAINTS, AccessConstraintValidator.INSTANCE);
        VALID_ATTRIBUTE_KEYS = Collections.unmodifiableMap(validAttributeKeys);

        Map<String, ArbitraryDescriptorValidator> validOperationKeys = new HashMap<String, ArbitraryDescriptorValidator>();
        validOperationKeys.put(DESCRIPTION, NullDescriptorValidator.INSTANCE);
        validOperationKeys.put(OPERATION_NAME, NullDescriptorValidator.INSTANCE);
        validOperationKeys.put(REQUEST_PROPERTIES, NullDescriptorValidator.INSTANCE);
        validOperationKeys.put(REPLY_PROPERTIES, NullDescriptorValidator.INSTANCE);
        validOperationKeys.put(DEPRECATED, DeprecatedDescriptorValidator.INSTANCE);
        validOperationKeys.put(ACCESS_CONSTRAINTS, AccessConstraintValidator.INSTANCE);
        VALID_OPERATION_KEYS = Collections.unmodifiableMap(validOperationKeys);

        Map<String, AttributeOrParameterArbitraryDescriptorValidator> validParameterKeys = new HashMap<String, AttributeOrParameterArbitraryDescriptorValidator>();
        validParameterKeys.putAll(paramAndAttributeKeys);
        VALID_PARAMETER_KEYS = Collections.unmodifiableMap(validParameterKeys);
    }

    private final List<ValidationFailure> errors;
    private final ModelNode address;
    private final ModelNode description;
    private final ValidationConfiguration validationConfiguration;

    /**
     * Constructor
     *
     * @param address the address of the model
     * @param description the description of the model
     * @param validationConfiguration extra configuration
     */
    public ModelTestModelDescriptionValidator(ModelNode address, ModelNode description, ValidationConfiguration validationConfiguration) {
        this(new ArrayList<ValidationFailure>(), address, description, validationConfiguration);
    }

    private ModelTestModelDescriptionValidator(List<ValidationFailure> errors, ModelNode address, ModelNode description, ValidationConfiguration validationConfiguration) {
        if (address == null) {
            throw new IllegalArgumentException("Null address");
        }
        if (description == null) {
            throw new IllegalArgumentException("Null address");
        }
        this.errors = errors;
        this.address = address;
        this.description = description;
        this.validationConfiguration = validationConfiguration == null ? new ValidationConfiguration() : validationConfiguration;
    }

    public List<ValidationFailure> validateResources() {
        if (!description.isDefined()) {
            errors.add(new MessageValidationFailure("No model", address));
            return errors;
        }
        for (String key : description.keys()) {
            ArbitraryDescriptorValidator validator = VALID_RESOURCE_KEYS.get(key);
            if (validator == null) {
                errors.add(new MessageValidationFailure("Invalid key '" + key + "'", address));
            } else {
                if (description.hasDefined(key)) {
                    String error = validator.validate(description, key);
                    if (error != null) {
                        errors.add(new MessageValidationFailure(error, address));
                    }
                } else {
                    if (!key.equals(OPERATIONS) && !key.equals(CHILDREN)) {
                        errors.add(new MessageValidationFailure("Empty key '" + key + "'", address));
                    }
                }

            }
        }
        if (!description.hasDefined(DESCRIPTION)) {
            errors.add(new MessageValidationFailure("Missing description", address));
        }
        if (description.hasDefined("attributes")) {
            validateAttributes(description.get("attributes"));
        }
        if (description.hasDefined("operations")) {
            validateOperations(description.get("operations"));
        }
        if (description.hasDefined("children")) {
            validateChildren(description.get("children"));
        }

        return errors;
    }

    private void validateAttributes(ModelNode attributes) {
        for (String key : attributes.keys()) {
            ModelNode attribute = attributes.get(key);
            AttributeValidationElement attributeValidationElement = new AttributeValidationElement(key);
            validateAttributeOrParameter(attributeValidationElement, attribute);
        }
    }

    private void validateOperations(ModelNode operations) {
        for (String key : operations.keys()) {
            OperationValidationElement operation = new OperationValidationElement(key);
            ModelNode op = operations.get(key);
            operation.validateKeys(op);

            if (!op.hasDefined(DESCRIPTION)) {
                errors.add(operation.createValidationFailure("Missing description"));
            }
            if (!key.equals(op.get(OPERATION_NAME).asString())) {
                errors.add(operation.createValidationFailure("Expected operation-name '" + key + "'"));
            }
            if (op.hasDefined(REQUEST_PROPERTIES)) {
                for (String param : op.get(REQUEST_PROPERTIES).keys()) {
                    ModelNode paramNode = op.get(REQUEST_PROPERTIES, param);
                    OperationParameterValidationElement parameterValidationElement = new OperationParameterValidationElement(operation, param);
                    validateAttributeOrParameter(parameterValidationElement, paramNode);
                }
            }
            if (op.hasDefined(REPLY_PROPERTIES)) {
                ModelNode reply = op.get(REPLY_PROPERTIES);
                OperationParameterValidationElement parameterValidationElement = new OperationParameterValidationElement(operation, REPLY_PROPERTIES);
                validateAttributeOrParameter(parameterValidationElement, reply);
            }
        }
    }

    private void validateChildren(ModelNode children) {
        for (String type : children.keys()) {
            ModelNode typeNode = children.get(type);
            for (String key : typeNode.keys()) {
                ArbitraryDescriptorValidator validator = VALID_CHILD_TYPE_KEYS.get(key);
                if (validator == null) {
                    errors.add(new MessageValidationFailure("Invalid key '" + key + "' found for child type '" + type + "'", address));
                } else {
                    if (typeNode.hasDefined(key)) {
                        String error = validator.validate(typeNode, key);
                        if (error != null) {
                            errors.add(new MessageValidationFailure(error, address));
                        }
                    } else {
                        if (!key.equals(MODEL_DESCRIPTION)) {
                            errors.add(new MessageValidationFailure("Empty key '" + key + "' found for child type '" + type + "'", address));
                        }
                    }
                }
            }

            if (!typeNode.hasDefined(DESCRIPTION)) {
                errors.add(new MessageValidationFailure("Missing description for child type '" + type + "' on ModelNode '" + typeNode.toString()+"'", address));
            }
            ModelNode childType = typeNode.get(MODEL_DESCRIPTION);
            if (!childType.isDefined()) {
                errors.add(new MessageValidationFailure("No model description for child '" + type + "'", address));
                continue;
            }

            for (String child : childType.keys()) {
                ModelNode childAddress = address.clone();
                childAddress.add(type, child);
                ModelTestModelDescriptionValidator childValidator = new ModelTestModelDescriptionValidator(errors, childAddress, childType.get(child), validationConfiguration);
                childValidator.validateResources();
            }
        }
    }

    private void validateAttributeOrParameter(AttributeValidationElement validationElement, ModelNode rawDescription) {
        boolean reply = validationElement.name.equals(REPLY_PROPERTIES);
        if (!rawDescription.hasDefined(DESCRIPTION) && !reply) {
            errors.add(validationElement.createValidationFailure("Missing description"));
        }
        if (reply && rawDescription.isDefined()) {
          if (rawDescription.keys().size() == 0) {
              return;
          }
        }
        validateAttributeOrParameter(validationElement, rawDescription, rawDescription);
    }

    private void validateAttributeOrParameter(AttributeValidationElement validationElement, ModelNode rawDescription, ModelNode currentDescription) {

        if (!currentDescription.isDefined()) {
            errors.add(validationElement.createValidationFailure("Undefined description " + currentDescription.asString()));
        }


        ModelNode typeNode = currentDescription.hasDefined(TYPE) ? currentDescription.get(TYPE) : null;
        ModelNode valueTypeNode = currentDescription.hasDefined(VALUE_TYPE) ? currentDescription.get(VALUE_TYPE) : null;

        ModelType type;
        try {
            type = typeNode.asType();
        } catch (Exception e) {
            errors.add(validationElement.createValidationFailure("Invalid type " + currentDescription.asString()));
            return;
        }

        if (type == ModelType.OBJECT || type == ModelType.LIST) {
            if (valueTypeNode == null && !validationElement.allowNullValueTypeForObject()) {
                errors.add(validationElement.createValidationFailure("No value-type for type=" + type + " " + currentDescription.asString()));
                return;
            }
        } else {
            if (valueTypeNode != null) {
                errors.add(validationElement.createValidationFailure("Unnecessary value-type for type=" + type + " " + currentDescription.asString()));
            }
        }

        if (valueTypeNode != null) {
            try {
                valueTypeNode.asType();
            } catch (Exception e) {
                //Complex type

                Set<String> keys;
                try {
                    keys = valueTypeNode.keys();
                } catch (Exception e1) {
                    errors.add(validationElement.createValidationFailure("Could not get keys for value-type for type=" + type + " " + currentDescription.asString()));
                    return;
                }
                for (String key : keys) {
                    validateAttributeOrParameter(validationElement, rawDescription, valueTypeNode.get(key));
                }
            }
        }
        validationElement.validateKeys(type, currentDescription);
    }

    /**
     * Validate an attribute or parameter descriptor
     */
    public interface AttributeOrParameterArbitraryDescriptorValidator {
        /**
         * Validate an attribute or parameter descriptor
         *
         * @param currentType the type of the parameter/attribute
         * @param currentNode the current attribute or parameter
         * @param descriptor the current descriptor being validated
         * @return {@code null} if it passed validation, or an error message describing the problem
         */
        String validate(ModelType currentType, ModelNode currentNode, String descriptor);
    }

    /**
     * Validate a resource or operation descriptor
     */
    public interface ArbitraryDescriptorValidator {
        /**
         * Validate a resource or operation arbitrary descriptor
         *
         * @param currentNode the current attribute or parameter
         * @param descriptor the current descriptor being validated
         * @return {@code null} if it passed validation, or an error message describing the problem
         */
        String validate(ModelNode currentNode, String descriptor);
    }

    private static class SimpleIntDescriptorValidator implements ArbitraryDescriptorValidator {
        static final SimpleIntDescriptorValidator INSTANCE = new SimpleIntDescriptorValidator();
        @Override
        public String validate(ModelNode currentNode, String descriptor) {
            if (currentNode.hasDefined(descriptor)) {
                try {
                    currentNode.get(descriptor).asInt();
                } catch (Exception e) {
                    return "'" + descriptor + "' is not an int";
                }

            }
            return null;
        }
    }

    private static class NumericDescriptorValidator implements AttributeOrParameterArbitraryDescriptorValidator {

        static final NumericDescriptorValidator INSTANCE = new NumericDescriptorValidator();

        public String validate(ModelType currentType, ModelNode currentNode, String descriptor) {
            if (currentNode.hasDefined(descriptor)) {
                if (currentType != ModelType.BIG_DECIMAL && currentType != ModelType.BIG_INTEGER &&
                        currentType != ModelType.DOUBLE && currentType != ModelType.INT && currentType != ModelType.LONG) {
                    return "Unnecessary '" + descriptor + "' for non-numeric type=" + currentType;
                }
                if (!descriptor.equals(UNIT)) {
                    try {
                        if (currentType == ModelType.BIG_DECIMAL) {
                            currentNode.get(descriptor).asBigDecimal();
                        } else if (currentType == ModelType.BIG_INTEGER) {
                            currentNode.get(descriptor).asBigInteger();
                        } else if (currentType == ModelType.DOUBLE) {
                            currentNode.get(descriptor).asDouble();
                        } else if (currentType == ModelType.INT) {
                            currentNode.get(descriptor).asInt();
                        } else if (currentType == ModelType.LONG) {
                            currentNode.get(descriptor).asLong();
                        }
                    } catch (Exception e) {
                        return "'" + descriptor + "' is not a " + currentType;

                    }
                }
            }
            return null;
        }
    }

    private static class LengthDescriptorValidator implements AttributeOrParameterArbitraryDescriptorValidator {

        static final LengthDescriptorValidator INSTANCE = new LengthDescriptorValidator();

        public String validate(ModelType currentType, ModelNode currentNode, String descriptor) {
            if (currentNode.hasDefined(descriptor)) {
                if (currentType != ModelType.LIST && currentType != ModelType.STRING &&
                        currentType != ModelType.BYTES) {
                    return "Unnecessary '" + descriptor + "' for non-numeric type=" + currentType;
                }
                try {
                    currentNode.get(descriptor).asLong();
                } catch (Exception e) {
                    return "'" + descriptor + "' is not a int/long";
                }
            }
            return null;
        }
    }

    private static class BooleanDescriptorValidator implements ArbitraryDescriptorValidator, AttributeOrParameterArbitraryDescriptorValidator {
        static final BooleanDescriptorValidator INSTANCE = new BooleanDescriptorValidator();

        public String validate(ModelType currentType, ModelNode currentNode, String descriptor) {
            if (currentNode.hasDefined(descriptor)) {
                try {
                    currentNode.get(descriptor).asBoolean();
                } catch (Exception e) {
                    return "'" + descriptor + "' is not a boolean";
                }
            }
            return null;
        }

        @Override
        public String validate(ModelNode currentNode, String descriptor) {
            return validate(null, currentNode, descriptor);
        }
    }

    private static class AccessTypeDescriptorValidator implements AttributeOrParameterArbitraryDescriptorValidator {
        static final AccessTypeDescriptorValidator INSTANCE = new AccessTypeDescriptorValidator();

        public String validate(ModelType currentType, ModelNode currentNode, String descriptor) {
            if (currentNode.hasDefined(descriptor)) {
                try {
                    AccessType.valueOf(currentNode.get(descriptor).asString().toUpperCase(Locale.ENGLISH).replace('-', '_'));
                } catch (Exception e) {
                    return "'" + descriptor + "=" + currentNode.get(descriptor) + "' is not an access type";
                }
            }
            return null;
        }
    }

    private static class StorageDescriptorValidator implements AttributeOrParameterArbitraryDescriptorValidator {
        static final StorageDescriptorValidator INSTANCE = new StorageDescriptorValidator();

        public String validate(ModelType currentType, ModelNode currentNode, String descriptor) {
            if (currentNode.hasDefined(descriptor)) {
                try {
                    AttributeAccess.Storage.valueOf(currentNode.get(descriptor).asString().toUpperCase(Locale.ENGLISH));
                } catch (Exception e) {
                    return "'" + descriptor + "' is not a storage type";
                }
            }
            return null;
        }
    }


    private static class NullDescriptorValidator implements ArbitraryDescriptorValidator, AttributeOrParameterArbitraryDescriptorValidator {
        static final NullDescriptorValidator INSTANCE = new NullDescriptorValidator();

        @Override
        public String validate(ModelType currentType, ModelNode currentNode, String descriptor) {
            return null;
        }

        @Override
        public String validate(ModelNode currentNode, String descriptor) {
            return null;
        }
    }

    private static class DeprecatedDescriptorValidator implements ArbitraryDescriptorValidator, AttributeOrParameterArbitraryDescriptorValidator {
        static final DeprecatedDescriptorValidator INSTANCE = new DeprecatedDescriptorValidator();
        @Override
        public String validate(ModelNode currentNode, String descriptor) {
            if (!currentNode.get(descriptor).hasDefined(SINCE)) {
                return "model key 'since' is missing on deprecated definition for '"+descriptor+"'";
            }
            if (!currentNode.get(descriptor).hasDefined(REASON)) {
                return "model key 'reason' is missing on deprecated definition for '"+descriptor+"'";
            }
            return null;
        }

        @Override
        public String validate(ModelType currentType, ModelNode currentNode, String descriptor) {
            return validate(currentNode,descriptor);
        }
    }

    private static class AccessConstraintValidator implements ArbitraryDescriptorValidator, AttributeOrParameterArbitraryDescriptorValidator {
        static final AccessConstraintValidator INSTANCE = new AccessConstraintValidator();
        @Override
        public String validate(ModelNode currentNode, String descriptor) {
            return null;
        }
        @Override
        public String validate(ModelType currentType, ModelNode currentNode, String descriptor) {
            return null;
        }
    }

    private static class StringListValidator implements ArbitraryDescriptorValidator, AttributeOrParameterArbitraryDescriptorValidator {
        static final StringListValidator INSTANCE = new StringListValidator();

        @Override
        public String validate(ModelType currentType, ModelNode currentNode, String descriptor) {
            return validate(currentNode, descriptor);
        }

        @Override
        public String validate(ModelNode currentNode, String descriptor) {
            if (currentNode.hasDefined(descriptor)) {
                List<ModelNode> list;
                try {
                    list = currentNode.asList();
                } catch (Exception e) {
                    return "'" + descriptor + "' is not a list";
                }
                for (ModelNode entry : list) {
                    try {
                        entry.asString();
                    } catch (Exception e) {
                        return "'" + descriptor + "' is not a string";
                    }

                }
            }
            return null;
        }
    }

    /**
     * Allows extra configuration of the validation
     */
    public static class ValidationConfiguration {
        private Map<ModelNode, Set<String>> nullValueTypeAttributes = new HashMap<ModelNode, Set<String>>();
        private Map<ModelNode, Map<String, Set<String>>> nullValueTypeOperations = new HashMap<ModelNode, Map<String, Set<String>>>();
        private Map<ModelNode, Map<String, Map<String, AttributeOrParameterArbitraryDescriptorValidator>>> attributeDescriptors = new HashMap<ModelNode, Map<String, Map<String, AttributeOrParameterArbitraryDescriptorValidator>>>();
        private Map<ModelNode, Map<String, Map<String, Map<String, AttributeOrParameterArbitraryDescriptorValidator>>>> operationParameterDescriptors = new HashMap<ModelNode, Map<String, Map<String, Map<String, AttributeOrParameterArbitraryDescriptorValidator>>>>();

        /**
         * Allow undefined value-types for a OBJECT or LIST type attribute. This should be used
         * sparingly since an undefined value-type is normally a problem with your model.
         *
         * @param address the address of the attribute
         * @param name the name of the attribute
         */
        public void allowNullValueTypeForAttribute(ModelNode address, String name) {
            Set<String> names = nullValueTypeAttributes.get(address);
            if (names == null) {
                names = new HashSet<String>();
                nullValueTypeAttributes.put(address, names);
            }
            names.add(name);
        }

        /**
         * Allow undefined value-types for OBJECT or LIST type for all parameters and return types for an operation.
         * This should be used sparingly since an undefined value-type is normally a problem with your model.
         *
         * @param address the address of the operation
         * @param operation the name of the operation
         */
        public void allowNullValueTypeForOperation(ModelNode address, String operation) {
            allowNullValueTypeForOperationParameter(address, operation, "*");
        }

        /**
         * Allow undefined value-types for OBJECT or LIST type for an operation's reply-properties.
         * This should be used sparingly since an undefined value-type is normally a problem with your model.
         *
         * @param address the address of the operation
         * @param operation the name of the operation
         */
        public void allowNullValueTypeForOperationReplyProperties(ModelNode address, String operation) {
            allowNullValueTypeForOperationParameter(address, operation, REPLY_PROPERTIES);
        }

        /**
         * Allow undefined value-types for OBJECT or LIST type for an operation's parameter.
         * This should be used sparingly since an undefined value-type is normally a problem with your model.
         *
         * @param address the address of the operation
         * @param operation the name of the operation
         * @param param the name of the parameter
         */
        public void allowNullValueTypeForOperationParameter(ModelNode address, String operation, String param) {
            Map<String, Set<String>> names = nullValueTypeOperations.get(address);
            if (names == null) {
                names = new HashMap<String, Set<String>>();
                nullValueTypeOperations.put(address, names);
            }
            Set<String> params = names.get(operation);
            if (params == null) {
                params = new HashSet<String>();
                names.put(operation, params);
            }
            params.add(param);
        }

        /**
         * Register an additional arbitrary descriptor for an attribute
         *
         * @param address the address of the attribute
         * @param name the name of the attribute
         * @param descriptor the arbitrary descriptor to register for the attribute
         * @param validator an implementation of a validator, may be {@code null}
         */
        public void registerAttributeArbitraryDescriptor(ModelNode address, String name, String descriptor, AttributeOrParameterArbitraryDescriptorValidator validator) {
            Map<String, Map<String, AttributeOrParameterArbitraryDescriptorValidator>> byName = attributeDescriptors.get(address);
            if (byName == null) {
                byName = new HashMap<String, Map<String, AttributeOrParameterArbitraryDescriptorValidator>>();
                attributeDescriptors.put(address, byName);
            }
            Map<String, AttributeOrParameterArbitraryDescriptorValidator> descriptors = byName.get(name);
            if (descriptors == null) {
                descriptors = new HashMap<String, AttributeOrParameterArbitraryDescriptorValidator>();
                byName.put(name, descriptors);
            }
            descriptors.put(descriptor, validator == null ? NullDescriptorValidator.INSTANCE : validator);
        }

        /**
         * Register an additional arbitrary descriptor for all of an operation's parameters and reply properties
         *
         * @param address the address of the operation
         * @param operation the name of the operation
         * @param descriptor the arbitrary descriptor to register for the operation's parameters
         * @param validator an implementation of a validator, may be {@code null}
         */
        public void registerArbitraryDescriptorForOperation(ModelNode address, String operation, String descriptor, AttributeOrParameterArbitraryDescriptorValidator validator) {
            registerArbitraryDescriptorForOperationParameter(address, operation, "*", descriptor, validator);
        }

        /**
         * Register an additional arbitrary descriptor for an operation's reply properties
         *
         * @param address the address of the operation
         * @param operation the name of the operation
         * @param descriptor the arbitrary descriptor to register for the operation's parameters
         * @param validator an implementation of a validator, may be {@code null}
         */
        public void registerArbitraryDescriptorForOperationReplyProperties(ModelNode address, String operation, String descriptor, AttributeOrParameterArbitraryDescriptorValidator validator) {
            registerArbitraryDescriptorForOperationParameter(address, operation, REPLY_PROPERTIES, descriptor, validator);
        }

        /**
         * Register an additional arbitrary descriptor for an operation's parameter
         *
         * @param address the address of the operation
         * @param operation the name of the operation
         * @param parameter the name of the parameter
         * @param descriptor the arbitrary descriptor to register for the operation's parameters
         * @param validator an implementation of a validator, may be {@code null}
         */
        public void registerArbitraryDescriptorForOperationParameter(ModelNode address, String operation, String parameter, String descriptor, AttributeOrParameterArbitraryDescriptorValidator validator) {
            Map<String, Map<String, Map<String, AttributeOrParameterArbitraryDescriptorValidator>>> byName = operationParameterDescriptors.get(address);
            if (byName == null) {
                byName = new HashMap<String, Map<String, Map<String, AttributeOrParameterArbitraryDescriptorValidator>>>();
                operationParameterDescriptors.put(address, byName);
            }
            Map<String, Map<String, AttributeOrParameterArbitraryDescriptorValidator>> params = byName.get(operation);
            if (params == null) {
                params = new HashMap<String, Map<String, AttributeOrParameterArbitraryDescriptorValidator>>();
                byName.put(operation, params);
            }
            Map<String, AttributeOrParameterArbitraryDescriptorValidator> descriptors = params.get(parameter);
            if (descriptors == null) {
                descriptors = new HashMap<String, ModelTestModelDescriptionValidator.AttributeOrParameterArbitraryDescriptorValidator>();
                params.put(parameter, descriptors);
            }
            descriptors.put(descriptor, validator == null ? NullDescriptorValidator.INSTANCE : validator);
        }


        private AttributeOrParameterArbitraryDescriptorValidator getAttributeValidator(ModelNode address, String name, String descriptor) {
            Map<String, Map<String, AttributeOrParameterArbitraryDescriptorValidator>>  byName = attributeDescriptors.get(address);
            if (byName == null) {
                return null;
            }
            Map<String, AttributeOrParameterArbitraryDescriptorValidator> descriptors = byName.get(name);
            if (descriptors == null) {
                return null;
            }
            return descriptors.get(descriptor);
        }

        private AttributeOrParameterArbitraryDescriptorValidator getOperationParameterValidator(ModelNode address, String operation, String parameter, String descriptor) {
            Map<String, Map<String, Map<String, AttributeOrParameterArbitraryDescriptorValidator>>>  byName = operationParameterDescriptors.get(address);
            if (byName == null) {
                return null;
            }
            Map<String, Map<String, AttributeOrParameterArbitraryDescriptorValidator>> params = byName.get(operation);
            if (params == null) {
                return null;
            }
            Map<String, AttributeOrParameterArbitraryDescriptorValidator> descriptors = params.get(parameter);
            if (descriptors != null) {
                AttributeOrParameterArbitraryDescriptorValidator validator = descriptors.get(descriptor);
                if (validator != null) {
                    return validator;
                }
            }
            descriptors = params.get("*");
            if (descriptors == null) {
                return null;
            }
            return descriptors.get(descriptor);
        }


        private boolean isAllowNullValueTypeForOperationParameter(ModelNode address, String operation, String param) {
            Map<String, Set<String>> operations = nullValueTypeOperations.get(address);
            if (operations == null) {
                return false;
            }
            Set<String> params = operations.get(operation);
            if (params == null) {
                return false;
            }
            return params.contains(param) || params.contains("*");
        }


        private boolean isAllowNullValueTypeForAttribute(ModelNode address, String name) {
            Set<String> names = nullValueTypeAttributes.get(address);
            if (names == null) {
                return false;
            }
            return names.contains(name);
        }
    }

    /**
     * Contains a validation error
     */
    public abstract static class ValidationFailure {
        /**
         * Formats the error string for printing
         *
         * @return the formatted error string
         */
        public abstract String toString();


        /**
         * Gets the address the validation error happened at
         *
         * @return the address
         */
        public abstract ModelNode getAddress();

        /**
         * Gets the name of the operation failing validation
         *
         * @return the operation name or {@code null} if it was not an operation
         */
        public abstract String getOperationName();


        /**
         * Gets the name of the operation parameter failing validation
         *
         * @return the operation parameter name or {@code null} if it was not an operation parameter
         */
        public abstract String getOperationParameterName();


        /**
         * Gets the name of the attribute failing validation
         *
         * @return the attribute name or {@code null} if it was not an attribute
         */
        public abstract String getAttributeName();
    }

    static class MessageValidationFailure extends ValidationFailure {
        protected final String message;
        protected final ModelNode address;
        protected MessageValidationFailure(String message, ModelNode address){
            this.message = message;
            this.address = address;
        }

        /**
         * Formats the error string for printing
         *
         * @return the formatted error string
         */
        public String toString() {
            return message + " @" + address;
        }

        /**
         * Gets the address the validation error happened at
         *
         * @return the address
         */
        public final ModelNode getAddress() {
            return address;
        }

        /**
         * Gets the name of the operation failing validation
         *
         * @return the operation name or {@code null} if it was not an operation
         */
        public String getOperationName() {
            return null;
        }

        /**
         * Gets the name of the operation parameter failing validation
         *
         * @return the operation parameter name or {@code null} if it was not an operation parameter
         */
        public String getOperationParameterName() {
            return null;
        }


        /**
         * Gets the name of the attribute failing validation
         *
         * @return the attribute name or {@code null} if it was not an attribute
         */
        public String getAttributeName() {
            return null;
        }
    }

    private static class OperationValidationFailure extends MessageValidationFailure {
        protected final String operationName;
        OperationValidationFailure(String message, ModelNode address, String operationName) {
            super(message, address);
            this.operationName = operationName;
        }

        public String getOperationName() {
            return operationName;
        }

        public String toString() {
            return message + " for operation '" + operationName +
                    "' @" + address;
        }

    }

    private static class OperationParameterValidationFailure extends OperationValidationFailure {
        private final String parameterName;
        OperationParameterValidationFailure(String message, ModelNode address, String operationName, String parameterName) {
            super(message, address, operationName);
            this.parameterName = parameterName;
        }

        public String getOperationParameterName() {
            return parameterName;
        }

        public String toString() {
            return message + " for operation parameter '" + operationName + "." + parameterName +
                        "' @" + address;
        }
    }

    private static class AttributeValidationFailure extends MessageValidationFailure {
        private final String attributeName;
        private AttributeValidationFailure(String message, ModelNode address, String attributeName) {
            super(message, address);
            this.attributeName = attributeName;
        }

        public String getAttributeName() {
            return attributeName;
        }

        public String toString() {
            return message + " for attribute '" + attributeName +
                        "'" + address;
        }

    }

    private abstract class ValidationElement {
        protected final String name;

        public ValidationElement(String name) {
            this.name = name;
        }

        abstract ValidationFailure createValidationFailure(String description);
    }

    private class OperationValidationElement extends ValidationElement {
        OperationValidationElement(String name) {
            super(name);
        }

        String getName() {
            return name;
        }

        @Override
        public String toString() {
            return "Operation '" + name + "' @" + address;
        }

        void validateKeys(ModelNode description) {
            if (!description.isDefined()) {
                errors.add(createValidationFailure("Missing description for operation"));
                return;
            }
            for (String opKey : description.keys()) {
                ArbitraryDescriptorValidator validator = VALID_OPERATION_KEYS.get(opKey);
                if (validator == null) {
                    errors.add(createValidationFailure("Invalid key '" + opKey + "'"));
                } else {
                    if (description.hasDefined(opKey)) {
                        String error = validator.validate(description, opKey);
                        if (error != null) {
                            errors.add(createValidationFailure(error));

                        }
                    } else {
                        errors.add(createValidationFailure("Empty key '" + opKey + "'"));
                    }
                }
            }
        }

        ValidationFailure createValidationFailure(String message) {
            return new OperationValidationFailure(message, address, name);
        }
    }

    private class AttributeValidationElement extends ValidationElement {
        final Map<String, AttributeOrParameterArbitraryDescriptorValidator> standardValidators;
        AttributeValidationElement(String name) {
            this(name, VALID_ATTRIBUTE_KEYS);
        }

        public AttributeValidationElement(String name, Map<String, AttributeOrParameterArbitraryDescriptorValidator> standardValidators) {
            super(name);
            this.standardValidators = standardValidators;
        }

        @Override
        public String toString() {
            return "Attribute '" + name + "' @" + address;
        }

        void validateKeys(ModelType currentType, ModelNode description) {
            for (String attrKey : description.keys()) {
                AttributeOrParameterArbitraryDescriptorValidator validator = standardValidators.get(attrKey);
                if (validator == null) {
                    validator = getExtraValidator(attrKey);
                    if (validator == null) {
                        errors.add(createValidationFailure("Invalid key '" + attrKey + "'"));
                    }
                } else {
                    if (description.hasDefined(attrKey)) {
                        String error = validator.validate(currentType, description, attrKey);
                        if (error != null) {
                            errors.add(createValidationFailure(error));
                        }
                    } else {
                        errors.add(createValidationFailure("Empty key '" + attrKey + "'"));
                    }
                }
            }
        }

        ValidationFailure createValidationFailure(String message) {
            return new AttributeValidationFailure(message, address, name);
        }

        AttributeOrParameterArbitraryDescriptorValidator getExtraValidator(String key) {
            return validationConfiguration.getAttributeValidator(address, name, key);
        }

        boolean allowNullValueTypeForObject() {
            return validationConfiguration.isAllowNullValueTypeForAttribute(address, name);
        }
    }

    private class OperationParameterValidationElement extends AttributeValidationElement {
        private final OperationValidationElement operation;

        OperationParameterValidationElement(OperationValidationElement operation, String name) {
            super(name, VALID_PARAMETER_KEYS);
            this.operation = operation;
        }

        @Override
        public String toString() {
            return "Parameter '" + name + "' in operation " + operation.getName() + "' @" + address;
        }

        ValidationFailure createValidationFailure(String message) {
            return new OperationParameterValidationFailure(message, address, operation.getName(), name);
        }

        AttributeOrParameterArbitraryDescriptorValidator getExtraValidator(String key) {
            return validationConfiguration.getOperationParameterValidator(address, operation.getName(), name, key);
        }


        boolean allowNullValueTypeForObject() {
            return validationConfiguration.isAllowNullValueTypeForOperationParameter(address, operation.getName(), name);
        }
    }
}
