Spring MVC: how to display formatted date values in JSP EL
Asked Answered
L

4

19

Here's a simple value bean annotated with Spring's new (as of 3.0) convenience @DateTimeFormat annotation (which as I understand replaces the pre-3.0 need for custom PropertyEditors as per this SO question):

import java.time.LocalDate;
import org.springframework.format.annotation.DateTimeFormat;

public class Widget {
  private String name;

  @DateTimeFormat(pattern = "MM/dd/yyyy")
  private LocalDate created;

  // getters/setters excluded
}

When biding the values from a form submission to this widget, the date format works flawlessly. That is, only date strings in the MM/dd/yyyy format will convert successfully to actual LocalDate objects. Great, we're halfway there.

However, I would also like to be able to also display the created LocalDate property in a JSP view in the same MM/dd/yyyy format using JSP EL like so (assuming my spring controller added a widget attribute to the model):

${widget.created}

Unfortunately, this will only display the default toString format of LocalDate (in yyyy-MM-dd format). I understand that if I use spring's form tags the date displays as desired:

<form:form commandName="widget">
  Widget created: <form:input path="created"/>
</form:form>

But I'd like to simply display the formatted date string without using the spring form tags. Or even JSTL's fmt:formatDate tag.

Coming from Struts2, the HttpServletRequest was wrapped in a StrutsRequestWrapper which enabled EL expressions like this to actually interrogate the OGNL value stack. So I'm wondering if spring provide something similar to this for allowing converters to execute?

EDIT

I also realize that when using spring's eval tag the date will display according the pattern defined in the @DateTimeFormat annotation:

<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<spring:eval expression="widget.created"/>

Interestingly, when using a custom PropertyEditor to format the date, this tag does NOT invoke that PropertyEditor's getAsText method and therefore defaults to the DateFormat.SHORT as described in the docs. In any event, I'd still like to know if there is a way to achieve the date formatting without having to use a tag--only using standard JSP EL.

Lands answered 17/12, 2014 at 0:42 Comment(1)
consider formatting the date in controller before handing over to the view?Beautiful
L
3

I was dispirited to learn that spring developers have decided not to integrate Unified EL (the expression language used in JSP 2.1+) with Spring EL stating:

neither JSP nor JSF have a strong position in terms of our development focus anymore.

But taking inspiration from the JIRA ticket cited, I created a custom ELResolver which, if the resolved value is a java.time.LocalDate or java.time.LocalDateTime, will attempt to pull the @DateTimeFormat pattern value in order to format the returned String value.

Here's the ELResolver (along with the ServletContextListener used to bootstrap it):

    public class DateTimeFormatAwareElResolver extends ELResolver implements ServletContextListener {
      private final ThreadLocal<Boolean> isGetValueInProgress = new ThreadLocal<>();

      @Override
      public void contextInitialized(ServletContextEvent event) {
        JspFactory.getDefaultFactory().getJspApplicationContext(event.getServletContext()).addELResolver(this);
      }

      @Override
      public void contextDestroyed(ServletContextEvent sce) {}

      @Override
      public Object getValue(ELContext context, Object base, Object property) {
        try {
          if (Boolean.TRUE.equals(isGetValueInProgress.get())) {
            return null;
          }

          isGetValueInProgress.set(Boolean.TRUE);
          Object value = context.getELResolver().getValue(context, base, property);
          if (value != null && isFormattableDate(value)) {
            String pattern = getDateTimeFormatPatternOrNull(base, property.toString());
            if (pattern != null) {
              return format(value, DateTimeFormatter.ofPattern(pattern));
            }
          }
          return value;
        }
        finally {
          isGetValueInProgress.remove();
        }
      }

      private boolean isFormattableDate(Object value) {
        return value instanceof LocalDate || value instanceof LocalDateTime;
      }

      private String format(Object localDateOrLocalDateTime, DateTimeFormatter formatter) {
        if (localDateOrLocalDateTime instanceof LocalDate) {
          return ((LocalDate)localDateOrLocalDateTime).format(formatter);
        }
        return ((LocalDateTime)localDateOrLocalDateTime).format(formatter);
      }

      private String getDateTimeFormatPatternOrNull(Object base, String property) {
        DateTimeFormat dateTimeFormat = getDateTimeFormatAnnotation(base, property);
        if (dateTimeFormat != null) {
          return dateTimeFormat.pattern();
        }

        return null;
      }

      private DateTimeFormat getDateTimeFormatAnnotation(Object base, String property) {
        DateTimeFormat dtf = getDateTimeFormatFieldAnnotation(base, property);
        return dtf != null ? dtf : getDateTimeFormatMethodAnnotation(base, property);
      }

      private DateTimeFormat getDateTimeFormatFieldAnnotation(Object base, String property) {
        try {
          if (base != null && property != null) {
            Field field = base.getClass().getDeclaredField(property);
            return field.getAnnotation(DateTimeFormat.class);
          }
        }
        catch (NoSuchFieldException | SecurityException ignore) {
        }
        return null;
      }

      private DateTimeFormat getDateTimeFormatMethodAnnotation(Object base, String property) {
        try {
          if (base != null && property != null) {
            Method method = base.getClass().getMethod("get" + StringUtils.capitalize(property));
            return method.getAnnotation(DateTimeFormat.class);
          }
        }
        catch (NoSuchMethodException ignore) {
        }
        return null;
      }

      @Override
      public Class<?> getType(ELContext context, Object base, Object property) {
        return null;
      }

      @Override
      public void setValue(ELContext context, Object base, Object property, Object value) {
      }

      @Override
      public boolean isReadOnly(ELContext context, Object base, Object property) {
        return true;
      }

      @Override
      public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base) {
        return null;
      }

      @Override
      public Class<?> getCommonPropertyType(ELContext context, Object base) {
        return null;
      }
    }

Register the ELResolver in web.xml:

<listener>
  <listener-class>com.company.el.DateTimeFormatAwareElResolver</listener-class>
</listener>

And now when I have ${widget.created} in my jsp, the value displayed will be formatted according to the @DateTimeFormat annotation!

Additionally, if the LocalDate or LocalDateTime object is needed by the jsp (and not just the formatted String representation), you can still access the object itself using direct method invocation like: ${widget.getCreated()}

Lands answered 20/5, 2016 at 17:5 Comment(2)
In your solution, is isGetValueInProgress necessary, and if so why?Rand
nevermind, found a comment in an old github repo that led me to the purpose. // This resolver is in the original resolver chain. To prevent // infinite recursion, set a flag to prevent this resolver from // invoking the original resolver chain again when its turn in the // chain comes around.Rand
Y
9

You may use the tag to provide you these kind of formattings, such as money, data, time, and many others.

You may add on you JSP the reference: <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

And use the formatting as: <fmt:formatDate pattern="yyyy-MM-dd" value="${now}" />

Follows below a reference:

http://www.tutorialspoint.com/jsp/jstl_format_formatdate_tag.htm

Yonit answered 11/3, 2015 at 22:49 Comment(1)
The <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> taglib does not work with java.time.LocalDateOvertop
S
4

To precise Eduardo answer:

<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

<fmt:formatDate pattern="MM/dd/yyyy" value="${widget.created}" />
Susannahsusanne answered 24/3, 2015 at 22:59 Comment(2)
As I mentioned in the question, I was hoping to be able to output the value directly without using JSTL's fmt:formatDate tag. (And in any event this tag does not work when using LocalDate date objects.)Lands
Why you don't want to use the JSTL?Susannahsusanne
P
3

I also prefer to not do any formatting via tags. I realise this may not be the solution you are looking for and are looking for a way to do this via spring annotations. Nevertheless, In the past I've used the following work around:

Create a new getter with the following signature:

public String getCreatedDateDisplay

(You can alter the name of the getter if you prefer.)

Within the getter, format the created date attribute as desired using a formatter such as SimpleDateFormat.

Then you can call the following from your JSP

${widget.createDateDisplay}
Philpot answered 6/1, 2015 at 0:36 Comment(1)
This is a valid alternative. Thanks for the answer. But you're right--I'm really hoping that I can use Spring's @DateTimeFormat and declaratively get what I need in the view rather than have to do the work (imperatively) in the bean. We're on the same page, though, because the end result of ${widget.createDateDisplay} is very nice (especially when dealing with the newer java.time.LocalDate which does not play as nice with JSTL's fmt taglib).Lands
L
3

I was dispirited to learn that spring developers have decided not to integrate Unified EL (the expression language used in JSP 2.1+) with Spring EL stating:

neither JSP nor JSF have a strong position in terms of our development focus anymore.

But taking inspiration from the JIRA ticket cited, I created a custom ELResolver which, if the resolved value is a java.time.LocalDate or java.time.LocalDateTime, will attempt to pull the @DateTimeFormat pattern value in order to format the returned String value.

Here's the ELResolver (along with the ServletContextListener used to bootstrap it):

    public class DateTimeFormatAwareElResolver extends ELResolver implements ServletContextListener {
      private final ThreadLocal<Boolean> isGetValueInProgress = new ThreadLocal<>();

      @Override
      public void contextInitialized(ServletContextEvent event) {
        JspFactory.getDefaultFactory().getJspApplicationContext(event.getServletContext()).addELResolver(this);
      }

      @Override
      public void contextDestroyed(ServletContextEvent sce) {}

      @Override
      public Object getValue(ELContext context, Object base, Object property) {
        try {
          if (Boolean.TRUE.equals(isGetValueInProgress.get())) {
            return null;
          }

          isGetValueInProgress.set(Boolean.TRUE);
          Object value = context.getELResolver().getValue(context, base, property);
          if (value != null && isFormattableDate(value)) {
            String pattern = getDateTimeFormatPatternOrNull(base, property.toString());
            if (pattern != null) {
              return format(value, DateTimeFormatter.ofPattern(pattern));
            }
          }
          return value;
        }
        finally {
          isGetValueInProgress.remove();
        }
      }

      private boolean isFormattableDate(Object value) {
        return value instanceof LocalDate || value instanceof LocalDateTime;
      }

      private String format(Object localDateOrLocalDateTime, DateTimeFormatter formatter) {
        if (localDateOrLocalDateTime instanceof LocalDate) {
          return ((LocalDate)localDateOrLocalDateTime).format(formatter);
        }
        return ((LocalDateTime)localDateOrLocalDateTime).format(formatter);
      }

      private String getDateTimeFormatPatternOrNull(Object base, String property) {
        DateTimeFormat dateTimeFormat = getDateTimeFormatAnnotation(base, property);
        if (dateTimeFormat != null) {
          return dateTimeFormat.pattern();
        }

        return null;
      }

      private DateTimeFormat getDateTimeFormatAnnotation(Object base, String property) {
        DateTimeFormat dtf = getDateTimeFormatFieldAnnotation(base, property);
        return dtf != null ? dtf : getDateTimeFormatMethodAnnotation(base, property);
      }

      private DateTimeFormat getDateTimeFormatFieldAnnotation(Object base, String property) {
        try {
          if (base != null && property != null) {
            Field field = base.getClass().getDeclaredField(property);
            return field.getAnnotation(DateTimeFormat.class);
          }
        }
        catch (NoSuchFieldException | SecurityException ignore) {
        }
        return null;
      }

      private DateTimeFormat getDateTimeFormatMethodAnnotation(Object base, String property) {
        try {
          if (base != null && property != null) {
            Method method = base.getClass().getMethod("get" + StringUtils.capitalize(property));
            return method.getAnnotation(DateTimeFormat.class);
          }
        }
        catch (NoSuchMethodException ignore) {
        }
        return null;
      }

      @Override
      public Class<?> getType(ELContext context, Object base, Object property) {
        return null;
      }

      @Override
      public void setValue(ELContext context, Object base, Object property, Object value) {
      }

      @Override
      public boolean isReadOnly(ELContext context, Object base, Object property) {
        return true;
      }

      @Override
      public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base) {
        return null;
      }

      @Override
      public Class<?> getCommonPropertyType(ELContext context, Object base) {
        return null;
      }
    }

Register the ELResolver in web.xml:

<listener>
  <listener-class>com.company.el.DateTimeFormatAwareElResolver</listener-class>
</listener>

And now when I have ${widget.created} in my jsp, the value displayed will be formatted according to the @DateTimeFormat annotation!

Additionally, if the LocalDate or LocalDateTime object is needed by the jsp (and not just the formatted String representation), you can still access the object itself using direct method invocation like: ${widget.getCreated()}

Lands answered 20/5, 2016 at 17:5 Comment(2)
In your solution, is isGetValueInProgress necessary, and if so why?Rand
nevermind, found a comment in an old github repo that led me to the purpose. // This resolver is in the original resolver chain. To prevent // infinite recursion, set a flag to prevent this resolver from // invoking the original resolver chain again when its turn in the // chain comes around.Rand

© 2022 - 2024 — McMap. All rights reserved.