javaspring-bootfactory-pattern

Designing a sophisticated validation framework: Find correct validator from factory


I am designing a validation framework that will handle many different types of validations based on the object that I pass to my Validator class. Below is the Factory pattern based iplementation of the same.

Validator.java


/**
 * Validator interface
 *
 * @param <T> generic type
 * @param <M> generic type
 *
 */
@FunctionalInterface
public interface Validator<T, M extends Serializable> {
    /**
     * Validates target object
     *
     * @param object target object
     * @return map of errors (empty if valid)
     */
    Map<String, M> validate(T object);

    /**
     * Validates target object and throws exception in case
     * if list of errors is not empty
     *
     * @param object target object
     * @throws ValidationException if any validation errors exist
     */
    default void validateAndThrow(T object) throws ValidationException {
        Map<String, M> errors = validate(object);
        if (!errors.isEmpty()) {
            throw new ValidationException(errors);
        }
    }

    /**
     * Allows to configure validator if necessary
     *
     * @param visitor Validator visitor
     */
    default void configure(ValidatorVisitor visitor) {
        visitor.visit(this);
    }

    /**
     * Validator visitor functional interface
     */
    @FunctionalInterface
    interface ValidatorVisitor {

        /**
         * Action to be performed on a validator
         *
         * @param validator target validator
         */
        void visit(Validator validator);
    }
}

ValidatorFactory.java



/**
 * Implementation of validation factory
 *
 * Created by Saransh Bansal on 15/05/2020
 */
@Component
public class ValidatorFactory {

    @Autowired
    List<Validator> validators;

    /**
     * Returns specific validator based on object's class.
     *
     * @param object target object
     * @return instance of {@link Validator}
     */
    public Validator getValidatorForObject(Object object) {
        return validators.stream()
                .filter(v -> {
                    System.out.println("....." + v.getClass());
                    // return v.getClass().isAssignableFrom(object.getClass()); - ???
                })
                .findFirst()
                .orElseThrow(() -> new GenericRuntimeException(ERROR_TYPE_VALIDATOR_NOT_FOUND.getText(
                        object == null ? "null" : object.getClass())));
    }
}

Custom validators - DocumentValidator.java


/**
 * Document upload Request validator
 *
 */
@Component
public class DocumentValidator implements Validator<DocumentDto, Serializable> {
    private static final long DEFAULT_MAX_FILE_SIZE = 5 * 1024L * 1024;

    @Value("${aap.apricot.document.allowedContentTypes:}#{T(java.util.Collections).emptyList()}")
    private List<String> allowedContentTypes;

    @Value("${aap.apricot.document.allowedFileNames:}#{T(java.util.Collections).emptyList()}")
    private List<String> allowedFileNames;

    @Value("${aap.apricot.document.max.size:" + DEFAULT_MAX_FILE_SIZE + "}")
    private long maxFileSize;

    @Override
    public Map<String, Serializable> validate(DocumentDto documentData) {
        notNull(documentData, ERROR_MSG_DOCUMENT_MISSED.getText());

        Map<String, Serializable> errors = new HashMap<>();

        if (isNull(documentData.getFile())) {
            errors.put(FIELD_DOCUMENT_FILE.getText(), ERROR_MSG_DOCUMENT_FILE_MISSED.getText());
        } else {
            String contentType = documentData.getFile().getContentType();
            String fileName = documentData.getFile().getOriginalFilename();

            if (isNull(documentData.getBusinessDate())) {
                errors.put(FIELD_DOCUMENT_FILE.getText(), buildMessage(ERROR_MSG_DOCUMENT_BUSINESS_DATE_MISSED.getText()));
            }
            if (documentData.getFile().getSize() > maxFileSize) {
                errors.put(FIELD_DOCUMENT_FILE.getText(), buildMessage(ERROR_MSG_DOCUMENT_FILE_SIZE_EXCEEDED.getText(), maxFileSize));
            }
            if (!isEmpty(allowedContentTypes) && contentType != null &&
                    allowedContentTypes.stream().noneMatch(contentType::equalsIgnoreCase)) {
                errors.put(FIELD_DOCUMENT_FILE.getText(), buildMessage(ERROR_MSG_DOCUMENT_RESTRICTED_CONTENT_TYPE.getText(), contentType));
            }
            if (!isEmpty(allowedFileNames) && fileName != null &&
                        allowedFileNames.stream().noneMatch(fileName::startsWith)) {
                errors.put(FIELD_DOCUMENT_FILE.getText(), buildMessage(ERROR_MSG_DOCUMENT_RESTRICTED_FILE_NAME.getText(), fileName));
            }
        }
        return errors;
    }
}

I cannot seem to figure out how to correctly return a validator from my Factory class. (See the code commented with ??? in ValidatorFactory.java)

Can anyone help with the same.


Solution

  • Based on my understanding your Factory needs to return based on a specific type.

    Rough Code to give you an idea:

    The ValidatorFactory is going to filter from its list of Validators, based on whether they support a given Object or not.

    Keep in mind if >1 Validator that supports a given Type, then your filter will return 2 or more. You will only return findFirst().

    Option 1:

    Public class ValidatorFactory {
    
        validators.stream().filter(v -> v.supports(object.getClass()).findFirst();
    
    
    }
    
    public class DocumentValidator{
    
      public boolean supports(Class clazz){
    
       return true;
       }
    }
    

    Option 2:

    public class ValidatorFactory{
      
       private Map<Class, Validator> validatorAssociations = new HashMap<>();
    
    
       public Validator getValidator(Object object){
     
          return validatorAssociations.get(object.class);
       }
      
    }
    

    You will need some sort of mapping, whether it is the responsibility of the ValidatorFactory (Which is generally the case for the FactoryPattern), or you push the responsibility down to the validator to know its own capabilities. Up to you.