6

After written several backend APIs, I found that the following code duplicates in almost every method which needs to filter data by dates:

@GetMapping(value="/api/test")
@ResponseBody
public Result test(@RequestParam(value = "since", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate since,
                   @RequestParam(value = "until", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate until) {
    // Date validation code I want to eliminate
    since = ObjectUtils.defaultIfNull(since, DEFAULT_SINCE_DATE);
    until = ObjectUtils.defaultIfNull(until, LocalDate.now().plusDays(1));
    if(since.isAfter(until)) {
        throw new SinceDateAfterUntilDateException();
    }

    // Do stuff
}

Obviously this is some kind of code smell. But, since I do need to validate since and until before using them to query the service/DAO, I am not sure where should I extract these code to?

Any advice?

Fred Pym
  • 2,149
  • 1
  • 20
  • 29
  • 1
    have you thought of using Aspects? – pvpkiran Jun 07 '17 at 10:00
  • Maybe in a method annotated with modelattribute? – Jack Flamp Jun 07 '17 at 20:40
  • I had the same problem, what I came up with was validating only that the "since" date is in the past, with custom requestparam validator. It fitted my problem since I had to retrieve past usage data. I know that this doesn't answer the question fully. – ArtyomH Apr 25 '20 at 02:07

4 Answers4

0
  1. Implement org.springframework.core.convert.converter.Converter; interface.
  2. Register with spring conversion service.
  3. Use in you controller Sharing sample code below :
public class MyCustomDateTypeConverter implements Converter<String, LocalDate> {

  @Override
  public LocalDate convert(String param) {
      //convert string to DateTime
      return dateTiemObjectCreatedfromParam;
  }

}
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">  <property name="converters">    <list>
        <bean class="com.x.y.z.web.converters.MyCustomDateTypeConverter"/>  </list>     </property> 
</bean>


<mvc:annotation-driven conversion-service="conversionService">

</mvc:annotation-driven>


public Result test(LocalDate since,LocalDate until) {

    since = ObjectUtils.defaultIfNull(since, DEFAULT_SINCE_DATE);
    until = ObjectUtils.defaultIfNull(until, LocalDate.now().plusDays(1));
    if(since.isAfter(until)) {
        throw new SinceDateAfterUntilDateException();
    }

    // Do stuff
}
Amit Parashar
  • 1,447
  • 12
  • 15
  • Thanks for your reply. But what I actually want to remove is the code checking the nullity and the precedence of the two variables:) – Fred Pym Jun 07 '17 at 12:16
0

As ol' gud-procedural approach suggests:

public Result test(@RequestParam(value = "since", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate since,
               @RequestParam(value = "until", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate until) {
     checkInputDates();
     // Do stuff
}

private checkInputDates(LocalDate since, LocalDate until) {
    since = ObjectUtils.defaultIfNull(since, DEFAULT_SINCE_DATE);
    until = ObjectUtils.defaultIfNull(until, LocalDate.now().plusDays(1));
    if(since.isAfter(until)) {
        throw new SinceDateAfterUntilDateException();
    }
}
//so on..
Oleksii Kyslytsyn
  • 2,458
  • 2
  • 27
  • 43
0

If you have request params received from object then you can do it by using Bean level Validation(JSR 303) and custom Date Deserializer by extending Jackson serializers. This way you dont have check the params for null.

public class yourBeanName{
 public LocalDate since;
 public LocalDate until;

  @JsonDeserialize(using = CustomDateDeserializer.class)
   public void setSince(LocalDate since) {
    this.since = since;
   }

    // same for until
}

 // Custom jackson Desealizer

  @Component
  public class CustomDateDeserializer extends StdDeserializer<Date> {
    @Override
    public Date deserialize(JsonParser jsonparser, DeserializationContext 
  context)
    throws IOException, JsonProcessingException {
    // Here check the date for null and assign default with your dateTimeFormat
    }

 }
Sangam Belose
  • 4,262
  • 8
  • 26
  • 48
  • He is talking about request params, not the body – Pau Jun 08 '17 at 06:52
  • @Pau Yes I know. That's the reason I mentioned if you are using object (i.e. Json as request). Accessing request parameters explicitly we have to manually check those for null. – Sangam Belose Jun 08 '17 at 07:23
0

I would suggest a model type holding the parameters since and until with a custom bean validation (using Lombok but you can also write getters and setters). The defaults are now field initializers:

@Ordered({"since", "until"})
@Data
public class DateRange {
  @NotNull
  @PastOrPresent
  private LocalDate since = DEFAULT_SINCE_DATE;
  @NotNull
  private LocalDate until = LocalDate.now().plusDays(1);
}

@GetMapping(value="/api/test")
@ResponseBody
public Result test(@Valid DateRange dateFilter) {
    // Do stuff
}

To make the validation work, you need a custom bean validation constraint:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = OrderedValidator.class)
public @interface Ordered {
    /** The property names with comparable values in the expected order. **/
    String[] value();

    String message() default "{com.stackoverflow.validation.Ordered.message}";

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

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

And the validator to check the constraint (sorry, litte generics hell to make it work for any kind of Comparable values instead of LocaleDate only):

public class OrderedValidator implements ConstraintValidator<Ordered, Object>
{
    private String[] properties;

    @Override
    public void initialize(Ordered constraintAnnotation) {
        if (constraintAnnotation.value().length < 2) {
            throw new IllegalArgumentException("at least two properties needed to define an order");
        }
        properties = constraintAnnotation.value();
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        return isValid(value));
    }

    private <T extends Comparable<? super T>> boolean isValid(Object value)
    {
        List<T> values = getProperties(value);
        return isSorted(values);
    }

    private <T extends Comparable<? super T>> List<T> getProperties(Object value)
    {
        BeanWrapperImpl bean = new BeanWrapperImpl(value);
        return Stream.of(properties)
            .map(bean::getPropertyDescriptor)
            .map(pd -> this.<T>getProperty(pd, value))
            .filter(Objects::nonNull)
            .collect(Collectors.toList());
    }

    // See https://stackoverflow.com/a/3047160/12890
    private <T extends Comparable<? super T>> boolean isSorted(Iterable<T> iterable) {
        Iterator<T> iter = iterable.iterator();
        if (!iter.hasNext()) {
            return true;
        }
        T t = iter.next();
        while (iter.hasNext()) {
            T t2 = iter.next();
            if (t.compareTo(t2) > 0) {
                return false;
            }
            t = t2;
        }
        return true;
    }

    @SuppressWarnings("unchecked")
    private <T extends Comparable<? super T>> T getProperty(PropertyDescriptor prop, Object bean) {
        try {
            return prop.getReadMethod() == null ? null : (T)prop.getReadMethod().invoke(bean);
        } catch (ReflectiveOperationException noAccess) {
            return null;
        }
    }
}
Arne Burmeister
  • 20,046
  • 8
  • 53
  • 94