ValidCropTraitObservationValidator.java

/*
 * Copyright 2026 Global Crop Diversity Trust
 * Licensed under the Apache License, Version 2.0
 * See LICENSE file in project root folder or http://www.apache.org/licenses/LICENSE-2.0
 */

package org.gringlobal.custom.validation.javax;

import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.util.Objects;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import org.gringlobal.model.CropTraitObservation;
import org.gringlobal.model.community.CommunityCodeValues;

/**
 * Validate that CTO values match CropTrait definition.
 *
 * NOTE: Keep this in sync with ValidCropTraitObservationDataValidator
 */
public class ValidCropTraitObservationValidator implements ConstraintValidator<ValidCropTraitObservation, CropTraitObservation> {

	@Override
	public boolean isValid(CropTraitObservation cto, ConstraintValidatorContext context) {
		try {
			if (cto == null) {
				return true;
			}
			var cropTrait = cto.getCropTrait();
			if (cropTrait == null) {
				context.buildConstraintViolationWithTemplate("cropTrait must be non-null")
					.addPropertyNode("cropTrait")
					.addConstraintViolation();
				return false;
			}

			var numericValue = cto.getNumericValue();
			var stringValue = cto.getStringValue();
			if (numericValue == null && stringValue == null) return true; // No data to validate

			var isValid = true;
			var dataTypeCode = cropTrait.getDataTypeCode();

			// Check NUMERIC type
			if (Objects.equals(CommunityCodeValues.CROP_TRAIT_DATA_TYPE_NUMERIC.value, dataTypeCode)) {
				if (numericValue != null) {
					if (cropTrait.getNumericMinimum() != null && numericValue < cropTrait.getNumericMinimum()) {
						context.buildConstraintViolationWithTemplate("numericValue must be more than " + cropTrait.getNumericMinimum())
							.addPropertyNode("numericValue")
							.addConstraintViolation();
							isValid = false;
					}
					if (cropTrait.getNumericMaximum() != null && numericValue > cropTrait.getNumericMaximum()) {
						context.buildConstraintViolationWithTemplate("numericValue must be less than " + cropTrait.getNumericMinimum())
							.addPropertyNode("numericValue")
							.addConstraintViolation();
						isValid = false;
					}
				}
				// Must not have stringValue
				if (stringValue != null) {
					context.buildConstraintViolationWithTemplate("stringValue must be null for NUMERIC traits")
						.addPropertyNode("stringValue")
						.addConstraintViolation();
					isValid = false;
				}

			// Check DATE type
			} else if (Objects.equals(CommunityCodeValues.CROP_TRAIT_DATA_TYPE_DATE.value, dataTypeCode)) {
				if (stringValue != null) {
					try {
						LocalDate.parse(stringValue);
					} catch (DateTimeParseException e) {
						context.buildConstraintViolationWithTemplate("stringValue must be a valid date")
							.addPropertyNode("stringValue")
							.addConstraintViolation();
						isValid = false;
					}
				}
				// Must not have numericValue
				if (numericValue != null) {
					context.buildConstraintViolationWithTemplate("numericValue must be null for DATE traits")
						.addPropertyNode("numericValue")
						.addConstraintViolation();
					isValid = false;
				}

			// Check CHAR type
			} else if (Objects.equals(CommunityCodeValues.CROP_TRAIT_DATA_TYPE_CHAR.value, dataTypeCode)) {
				if (stringValue != null) {
					if (cropTrait.getMaxLength() != null) {
						if (stringValue.length() > cropTrait.getMaxLength()) {
							context.buildConstraintViolationWithTemplate("stringValue too long")
								.addPropertyNode("stringValue")
								.addConstraintViolation();
							isValid = false;
						}
					}
				}
				// Must not have numericValue
				if (numericValue != null) {
					context.buildConstraintViolationWithTemplate( "numericValue must be null for CHAR traits")
						.addPropertyNode("numericValue")
						.addConstraintViolation();
					isValid = false;
				}
			}

			return isValid;
		} catch (Throwable e) {
			return false;
		}
	}
}