0

I have a list of poperties defined in a config file, but before I proceed with further computations I would like to check if the properties have consistent values. Currently I am just doing via ifs.

private static void checkprops (Properties properties) throws Throwable {
        if (!properties.getProperty("number").equals("one")
                && !properties.getProperty("number").equals("two")) {
            throw new Exception("ERROR");
        }

        if (!properties.getProperty("check").equals("false")
                && !properties.getProperty("check").equals("true")) {
            throw new Exception("ERROR");
        }

        if (properties.getProperty("totalnum").equals("null")) {
            throw new Exception("ERROR");
        }

    }

Is there any way to do it somehow shorter and easier to read, since I have some properties that will have 5-6 different options.

Cap
  • 353
  • 2
  • 16

3 Answers3

0

You can create a utility method that would accept value from your configuration file and expected values as parameter and return a bool, for example:

public boolean validateProp(T propVal, T... expectedVals) {
    for(T expectedVal : expectedVals) {
        if(propVal == null) {
            if(expectedVal == null) {
                return true;
            }
        }
        else if(propVal.equals(expectedVal)) {
            return true;
        }
    }
    return false;
}

A sample call in that case would be:

if(!validateProp(properties.getProperty("number"), "one", "two") {
    throw new Exception("ERROR");
}
0

First of all let me point out, that your code has an error:

if (properties.getProperty("totalnum").equals("null")) { ... }

If the property is not defined, getProperty() returns null, thus your code would raise a NullPointerException when trying to access equals(). It does not return a string with value "null". That's the case for all your lines.


I'd approach this using reflection, with a config class that declares public fields that might be annotated with value checks. A method will then set values on an instance of that config class, reading from the properties map.

Further reading:

The advantage of this is that the config shows the valid values in an intuitive, speaking format. The drawback is that the "unmarshalling" code is a bit complex. though this approach is quite powerful.

The config could look like this:

static class Config {
    @RegExp("^(one|two)$")
    public String number;

    public Boolean check;

    @Required @Range(min=1, max=6)
    public Integer totalnum;
}

If a field is lacking the @Required annotation, a missing property does not result in an exception, thus the initialization value of Config is used.

Unmarshalling is done using this:

    Config config = new Config();
    setProperties(properties, config);

setProperties() will throw several exceptions when values are missing, have the wrong type or value. The exception can be catched and differentiated to display proper error messages.

In you application you can then access the config like simple objects:

if (config.totalnum == 3) {
    // do something when totalnum is 3
}

This is the unmarshalling code:

private void setProperties(Properties properties, Props props) throws SecurityException, IllegalArgumentException, IllegalAccessException {
    Class<?> clazz = props.getClass();

    for (Field field : clazz.getDeclaredFields()) {
        if ((field.getModifiers() & Modifier.PUBLIC) == 0) {
            // ignore non-public properties
            continue;
        }

        // the type of the field
        Class<?> fieldType = field.getType();
        // the field name of the class
        String fieldName = field.getName();
        // the raw value loaded from the .properties file 
        String value = properties.getProperty(fieldName);

        // fields may be annotated with @Required
        boolean required = (field.getAnnotation(Required.class) != null);

        if (required && value == null) {
            // field required but not defined in the properties, fail 
            throw new IllegalArgumentException(
                    String.format(
                            "Field %s is required",
                            fieldName
                    )
            );
        } else if (value == null) {
            // ignore undefined field, default to class initialized value
            continue;
        }

        // checks per type follow ...

        if (fieldType == String.class) {
            // fields may be annotated with RegExp to specify a matcher
            RegExp regExp = field.getAnnotation(RegExp.class);

            if (regExp != null && !Pattern.matches(regExp.value(), value)) {
                throw new IllegalArgumentException(
                        String.format(
                                "Value for field %s does not match %s: %s",
                                fieldName,
                                regExp.value(),
                                value
                        )
                );
            }

            field.set(props, value);
        } else if (fieldType == Integer.class) {
            // may throw NumberFormatException if not a valid integer
            Integer intValue = Integer.parseInt(value);

            // fields may be annotated with Range to specify an integer range
            Range range = field.getAnnotation(Range.class);

            if (range != null && !(intValue >= range.min() && intValue <= range.max())) {
                throw new IllegalArgumentException(
                        String.format(
                                "Value for field %s out of range (%d..%d): %d",
                                fieldName,
                                range.min(),
                                range.max(),
                                intValue
                        )
                );
            }

            field.set(props, intValue);
        } else if (fieldType == Boolean.class) {
            // strictly check valid boolean values 
            if (!Pattern.matches("^(true|false)$", value)) {
                throw new IllegalArgumentException(
                        String.format(
                                "Value for field %s is not a valid boolean (true|false): %s",
                                fieldName,
                                value
                        )
                );
            }

            field.set(props, Boolean.parseBoolean(value));
        }
    }
}

Though already quite complex this code is rather simple. It does not handle other number types like Long or primitive types like int yet. These can be implemented using further if branches.

These are the annotations (defined in separate classes):

@Retention(RUNTIME)
@Target(FIELD)
public @interface Range {
    public int min() default Integer.MIN_VALUE;
    public int max() default Integer.MAX_VALUE;
}

@Retention(RUNTIME)
@Target(FIELD)
public @interface RegExp {
    public String value() default "^.*$";
}

@Retention(RUNTIME)
@Target(FIELD)
public @interface Required {

}
try-catch-finally
  • 7,436
  • 6
  • 46
  • 67
0

An option to avoid validating the Properties object directly is to map it to a POJO and then use Bean Validation on it.

Bean Validation is a standard Java API for specifying validation constraints and checking objects (or even method arguments) for validity, which relieves you from writing much of the code for modeling errors. The reference implementation is Hibernate Validator but, despite the name, you can use it standalone without using all of Hibernate.

As an example (which might need some additional work):

public class Config {
  @Pattern("^(one|two)$")
  private String number;

  @NotNull
  private Boolean check;

  @NotNull
  @Min(1)
  @Max(6)
  private Integer totalnum;

  public static Config fromProperties(Properties ps) {
    Config conf = new Config();
    conf.number = ps.getProperty("number");
    // fails right away if "check" is null 
    conf.check = Boolean.valueOf(ps.getProperty("check"));
    // fails right away if "totalnum" is null
    conf.totalnum = Integer.valueOf(ps.getProperty("totalnum"));
    return conf;
  }
}

And the calling code:

Validator validator = Validation.buildDefaultValidatorFactory()
  .getValidator();
Config config = Config.fromProperties(properties);
Set<ConstraintViolation<Config>> violations = validator.validate(config);
if (violations.isEmpty()) {
  // good to go
} else {
  // some code to print the errors
}

In the example, I'm using the @Pattern constraint to match using a regex but, since you mentioned some properties have 5-6 possible values, it might be better to define your own annotation like

  @Values({"one", "two", "three", "four"})
  private String number;
kewne
  • 2,208
  • 15
  • 11