Set @Max parameters from properties

Development environment

Introduction

Continuing from the last time, we are talking about Bean Validation. This time we have a Spring element: ghost :. Validators such as @ Max and @ Min prepared by Bean Validation are prepared, but since value is defined by long, these are used as arguments. Only integers can be specified.

It is natural because we verify the numerical value, but maybe we want to change the allowable value depending on the environment. There may be a scene like that. In such a case, it is convenient to be able to refer to the value from the property. So, I would like to create a @MaxFromProperty annotation that can also refer to the value set in the property.

This goal ↓

Setting allowed values


age.max=100

Check the numerical value by referring to the property


@MaxFromProperty("age.max")
private long age; //Check if it is 100 or less

You can specify the value directly


@MaxFromProperty("20")
private long age; //Check if it is 20 or less

Please note that the source and link of the completed version is attached at the end of this article, but this is not the optimal solution. So there is a better way! We are looking for opinions from those who say: bow_tone1:

Make annotations

Now, let's create an annotation. That said, the annotation itself has nothing to mention. Is it enough to set the value to String so that it can receive the property key?

MaxFromProperty.java


@Documented
@Constraint(validatedBy = { MaxFromPropertyValidator.class })
@Target({ METHOD, FIELD })
@Retention(RUNTIME)
public @interface MaxFromProperty {

	String message() default "{com.neriudon.example.validator.MaxFromProperty.message}";

	Class<?>[] groups() default {};

	Class<? extends Payload>[] payload() default {};

	String value();

	@Target({ METHOD, FIELD })
	@Retention(RUNTIME)
	@Documented
	@interface List {
		MaxFromProperty[] value();
	}
}

Make a validator

This is important.

Get property value

I use PropertyResolver to resolve property values, but I needPropertySources as an argument. In addition, property values are managed by appliedPropertySources in PropertySourcesPlaceholderConfigurer. So, I will put the code only in the important part

MaxFromPropertyValidator.java



//Generate resolver in constructor
public MaxFromPropertyValidator(PropertySourcesPlaceholderConfigurer configurer) {
	//Get a resolver that resolves a property value
	resolver = new PropertySourcesPropertyResolver(configurer.getAppliedPropertySources());
}

@Override
public void initialize(MaxFromProperty constraintAnnotation) {
	max = getMaxValue(constraintAnnotation.value());
}

private long getMaxValue(String key) {
	//Property value resolution
	//The second argument is the default value
	String value = resolver.getProperty(key, key);
	try {
		//Convert to number
		return Long.parseLong(value);
	} catch (NumberFormatException e) {
		//Exception handling
	}
}

Okay: v :. I'd like to do that ...: sweat:

Corresponds to multiple PropertySourcesPlaceholderConfigurer

Depending on the application, multiple PropertySourcesPlaceholderConfigurers may be defined, so the appliedPropertySources of each PropertySourcesPlaceholderConfigurer must be combined into one PropertySources.

So, since MutablePropertySources, which is the default implementation of PropertySources, manages multiple PropertySources in a list, insert the contents of appliedPropertySources of each PropertySourcesPlaceholderConfigurer into MutablePropertySources.

Modified version of MaxFromPropertyValidator constructor


public MaxFromPropertyValidator(List<PropertySourcesPlaceholderConfigurer> configurers) {
	MutablePropertySources propertySources = new MutablePropertySources();
	configurers.forEach(c -> c.getAppliedPropertySources().forEach(p -> {
		propertySources.addLast(temp);
	}));
	this.resolver = new PropertySourcesPropertyResolver(propertySources);
}

That's okay: v :. I'd like to do it ...: severe:

If you actually check the operation with PropertySourcesPlaceholderConfigurer defined in multiple beans, the expected operation may not be achieved.

Because

  1. PropertySource overrides ʻequals ()[https://github.com/spring-projects/spring-framework/blob/master/spring-core/src/main/java/org/springframework/ core / env / PropertySource.java # L135-L138) and if the names ofPropertySource are the same, true` is returned.
  2. PropertySourcesPlaceholderConfigurer The system property is ʻenvironmentProperties, and the property (= local property) read from the property file is localProperties` and the name is fixed.
  3. [MutablePropertySources](https://github.com/spring-projects/spring-framework/blob/master/spring-core/src/main/java/org/springframework/core/env/MutablePropertySources.java# When adding PropertySource to L104-L107), compare the already added PropertySource with ʻequals ()and if it istrue, delete the already added PropertySource. , PropertySource` is added.

In other words, this implementation only adds the contents of the last loaded PropertySourcesPlaceholderConfigurer to MutablePropertySources: confounded :.

So ... forcibly give it a unique name and add it to MutablePropertySources. Unfortunately, PropertySource didn't have a method to overwrite a name likesetName (), so refill CompositePropertySource with an alias and then add it to MutablePropertySources.

Constructor modified version


public MaxFromPropertyValidator(List<PropertySourcesPlaceholderConfigurer> configurers) {
	MutablePropertySources propertySources = new MutablePropertySources();
	configurers.forEach(c -> c.getAppliedPropertySources().forEach(p -> {
	//Refill with a uniquely named CompositePropertySource and add it to MutablePropertySources
	CompositePropertySource temp = new CompositePropertySource(
		c.toString().concat("$").concat(p.getName()));
			temp.addPropertySource(p);
			propertySources.addLast(temp);
		}));
	this.resolver = new PropertySourcesPropertyResolver(propertySources);

There is room for improvement here, though I say it myself: innocent :.

By the way, if there are beans of PropertySourcesPlaceholderConfigurer, configurer1 and configurer2, and the value of the order attribute is 1 and 2 respectively and the value of the localOverride attribute is false, then MutablePropertySources To

  1. System properties of configurer1
  2. Local properties of configurer1
  3. System properties of configurer2
  4. Local properties of configurer2

Since they are added in the order of, system properties will be duplicated, but I think it is difficult to improve while maintaining consistency with the settings of ʻorder and localOverride`.

Consider the case where there is no PropertySourcesPlaceholderConfigurer in the first place

So far, we have assumed that PropertySourcesPlaceholderConfigurer is bean-defined, but I think that some applications may not have bean-defined. In that case, set ʻEnvironment because ʻEnvironment is a subinterface of PropertyResolver.

Constructor modified version


public MaxFromPropertyValidator(List<PropertySourcesPlaceholderConfigurer> configurers, Environment environment) {
	if (configurers != null) {
		//Abbreviation
	} else {
		this.resolver = environment;
	}
}

Use the original MaxValidator as a reference ...

Now that the process of getting the value from the property is complete, all you have to do is compare the value to be verified with the maximum value. The @ Max validator class in the original hibernate-validator supports numbers expressed in Number and CharSequence types, so please refer to it.

This is a personal preference, but it is difficult to understand if there are multiple Java files of the validator class, so make MaxFromPropertyValidator an abstract class and inherit it to the inner classes for Number and CharSequence. At that time, by overriding the ʻisValid method with MaxFromPropertyValidatorand performing only null check, by making the concrete numerical comparison an abstract method, the class forNumber and CharSequencecan compare the numerical value. You can concentrate on it, and you can set the qualifier of the maximum value obtained byMaxFromPropertyValidator to private. Finally, the class specified in @ Constraint of the annotation class also specifies both for NumberandCharSequence`.

Please read the source of ↓ because it is difficult to understand if it is a sentence: stuck_out_tongue_closed_eyes:

Completed version

Annotation class

MaxFromProperty.java


@Documented
@Constraint(validatedBy = { MaxFromPropertyValidator.NumberMaxFromPropertyValidator.class,
		MaxFromPropertyValidator.CharSequenceMaxFromPropertyValidator.class })
@Target({ METHOD, FIELD })
@Retention(RUNTIME)
public @interface MaxFromProperty {

	String message() default "{com.neriudon.example.validator.MaxFromProperty.message}";

	Class<?>[] groups() default {};

	Class<? extends Payload>[] payload() default {};

	String value();

	@Target({ METHOD, FIELD })
	@Retention(RUNTIME)
	@Documented
	@interface List {
		MaxFromProperty[] value();
	}
}

Validator class

MaxFromPropertyValidator.java


public abstract class MaxFromPropertyValidator<T> extends ApplicationObjectSupport
		implements ConstraintValidator<MaxFromProperty, T> {

	private long max;

	private final PropertyResolver resolver;

	public MaxFromPropertyValidator(List<PropertySourcesPlaceholderConfigurer> configurers, Environment environment) {
		if (configurers != null) {
			MutablePropertySources propertySources = new MutablePropertySources();
			configurers.forEach(c -> c.getAppliedPropertySources().forEach(p -> {
				// named unique name.
				// Because MutablePropertySources override propertySources if defined same name.
				CompositePropertySource temp = new CompositePropertySource(
						c.toString().concat("$").concat(p.getName()));
				temp.addPropertySource(p);
				propertySources.addLast(temp);
			}));
			this.resolver = new PropertySourcesPropertyResolver(propertySources);
		} else {
			this.resolver = environment;
		}
	}

	@Override
	public void initialize(MaxFromProperty constraintAnnotation) {
		max = getMaxValue(constraintAnnotation.value());
	}

	@Override
	public boolean isValid(T value, ConstraintValidatorContext context) {
		// null values are valid
		if (value == null) {
			return true;
		}
		return compareToMaxValue(value, max);
	}

	/**
	 * compare target value to maximum value
	 * 
	 * @param value
	 *            target value
	 * @param max
	 *            maximum value
	 * @return true if value is less than or equal to max.
	 */
	protected abstract boolean compareToMaxValue(T value, long max);

	/**
	 * return max value.<br>
	 * if no value mapped key, convert key to long.
	 */
	private long getMaxValue(String key) {
		String value = resolver.getProperty(key, key);
		try {
			return Long.parseLong(value);
		} catch (NumberFormatException e) {
			throw new IllegalArgumentException(
					"failed to get int value from Property(key:" + key + ", value:" + value + ")");
		}
	}

	/**
	 * MaxFromPropertyValidator for Number
	 */
	public static class NumberMaxFromPropertyValidator extends MaxFromPropertyValidator<Number> {

		public NumberMaxFromPropertyValidator(List<PropertySourcesPlaceholderConfigurer> configurers,
				Environment environment) {
			super(configurers, environment);
		}

		@Override
		public boolean compareToMaxValue(Number value, long max) {
			// handling of NaN, positive infinity and negative infinity
			if (value instanceof Double) {
				if ((Double) value == Double.NEGATIVE_INFINITY) {
					return true;
				} else if (Double.isNaN((Double) value) || (Double) value == Double.POSITIVE_INFINITY) {
					return false;
				}
			} else if (value instanceof Float) {
				if ((Float) value == Float.NEGATIVE_INFINITY) {
					return true;
				} else if (Float.isNaN((Float) value) || (Float) value == Float.POSITIVE_INFINITY) {
					return false;
				}
			}
			if (value instanceof BigDecimal) {
				return ((BigDecimal) value).compareTo(BigDecimal.valueOf(max)) != 1;
			} else if (value instanceof BigInteger) {
				return ((BigInteger) value).compareTo(BigInteger.valueOf(max)) != 1;
			} else {
				long longValue = value.longValue();
				return longValue <= max;
			}
		}
	}

	/**
	 * MaxFromPropertyValidator for CharSequernce
	 */
	public static class CharSequenceMaxFromPropertyValidator extends MaxFromPropertyValidator<CharSequence> {

		public CharSequenceMaxFromPropertyValidator(List<PropertySourcesPlaceholderConfigurer> configurers,
				Environment environment) {
			super(configurers, environment);
		}

		@Override
		public boolean compareToMaxValue(CharSequence value, long max) {
			try {
				return new BigDecimal(value.toString()).compareTo(BigDecimal.valueOf(max)) != 1;
			} catch (NumberFormatException nfe) {
				return false;
			}
		}
	}
}

Try to test

For clarity

--The upper limit can be set by referring to the property file. --It is also possible to set the upper limit directly --You can also check fields of type CharSequence

Is divided.

@RunWith(SpringRunner.class)
@SpringBootTest
public class ValidationSampleApplicationTests {
	@Autowired
	private Validator validator;

	@Test
	public void maxFromPropertySampleNg() {
		MaxFromPropertySample sample = new MaxFromPropertySample(100);
		Set<ConstraintViolation<MaxFromPropertySample>> result = validator.validate(sample);
		assertThat(result.size(), is(1));
		result.stream().forEach(r -> {
			assertThat(r.getInvalidValue(), is(100L));
		});
	}

	@Test
	public void maxValueSetDirectlyNg() {
		MaxValueSetDirectly sample = new MaxValueSetDirectly(100);
		Set<ConstraintViolation<MaxValueSetDirectly>> result = validator.validate(sample);
		assertThat(result.size(), is(1));
		result.stream().forEach(r -> {
			assertThat(r.getInvalidValue(), is(100L));
		});
	}

	@Test
	public void maxFromPropertyForCharSequenceNg() {
		MaxFromPropertyForCharSequence sample = new MaxFromPropertyForCharSequence("100");
		Set<ConstraintViolation<MaxFromPropertyForCharSequence>> result = validator.validate(sample);
		assertThat(result.size(), is(1));
		result.stream().forEach(r -> {
			assertThat(r.getInvalidValue(), is("100"));
		});
	}
	//Specify a numerical value from the property file
	private static class MaxFromPropertySample {

		@MaxFromProperty("max")
		private long value;

		public MaxFromPropertySample(long value) {
			this.value = value;
		}
	}
	//Specify the numerical value directly
	private static class MaxValueSetDirectly {

		@MaxFromProperty("99")
		private long value;

		public MaxValueSetDirectly(long value) {
			this.value = value;
		}
	}
	//You can also check Char Sequence type numbers
	private static class MaxFromPropertyForCharSequence {

		@MaxFromProperty("max")
		private CharSequence value;

		public MaxFromPropertyForCharSequence(CharSequence value) {
			this.value = value;
		}
	}
}

JavaConfig class

TestConfig.java


@Configuration
@PropertySource("sample.properties")
public class TestConfig {

	@Bean
	public Validator getValidator() {
		return new LocalValidatorFactoryBean();
	}
}

Place sample.properties directly under the classpath.

sample.properties


max=99

in conclusion

Just get the value from the property. I tried to make it, but it was hard because I had a lot of thoughts. Since the Spring mechanism is used to resolve property values, annotations created in pure Java will not work, but I think there is demand, so I hope that Hibernate will provide it someday.

By the way, the part that sets PropertyResolver in the validator class can be used for general purposes, so it can be extended to @ Min and @ Size in the same way.

The code created this time is listed in GitHub, but it is not the optimal solution as described in the article, so please be careful when referring to it: stuck_out_tongue_winking_eye :.

Recommended Posts

Set @Max parameters from properties
Passing parameters from JSP with Servlet