Spring MVC - Obtaining Client Time Zone Information

[Updated: Jan 11, 2017, Created: Jan 10, 2017]

When a client sends HTTP request to the server, the request header is automatically populated with 'Accept-Language', which helps server side to get client's Locale. This header doesn't have any client's time zone information. There's currently no standard way or specification to obtain client's time zone information.



Spring support for Time zone

The LocaleContextResolver interface is an extension of LocaleResolver that may include time zone information. Spring currently provides three implementations of this interface: CookieLocaleResolver, FixedLocaleResolver and SessionLocaleResolver. In previous four tutorials, we saw how to use them to obtain client's Locale. One of these classes can be used to set Time Zone by using this method:

void setLocaleContext(HttpServletRequest request,
                      HttpServletResponse response,
                      LocaleContext localeContext)

Handler method support

A controller's handler method can have java.util.TimeZone (Java 6+) / java.time.ZoneId (on Java 8) parameters. These parameters are populated with corresponding values as determined by the active LocaleContextResolver. If no time zone is set by LocaleContextResolver#setLocaleContext() then system's time zone will be used. This doesn't apply to direct query of TimeZone from ContextLocaleResolver, which will return null, unless we populate it via LocalContextResolver#setLocleContext().

Following snippet is from ServletRequestMethodArgumentResolver.java which resolves the handler method arguments:

 public Object resolveArgument( ...... ) throws Exception {
   .....
    else if (TimeZone.class == paramType) {
     TimeZone timeZone = RequestContextUtils.getTimeZone(request);
     return (timeZone != null ? timeZone : TimeZone.getDefault());
    }
    else if ("java.time.ZoneId".equals(paramType.getName())) {
     return ZoneIdResolver.resolveZoneId(request);
    }
   .....
   .....
  @UsesJava8
  private static class ZoneIdResolver {

    public static Object resolveZoneId(HttpServletRequest request) {
     TimeZone timeZone = RequestContextUtils.getTimeZone(request);
     return (timeZone != null ? timeZone.toZoneId() : ZoneId.systemDefault());
    }
   }
 }

Following handler will always print system (the server computer) time zone and zone id, doesn't matter if client is in different time zone:

@RequestMapping("/test")
public void exampleHandler (TimeZone tz, ZoneId zid) {
    System.out.println(tz);
    System.out.println(zid);
}


So how to obtain client time zone?

One of the solution to get client time zone is to use client-side Javascript's Date API:

<script>
 var date = new Date()
 var offset = date.getTimezoneOffset()
</script>

Browser will send above offset from javascript to server side so that Spring can use LocaleContextResolver#setLocaleContext(). Let's see an example to achieve that.



Example

Creating a custom LocaleContextResolver

A custom LocaleContextResolver implementation going to delegate Locale responsibilities to AcceptHeaderLocaleResolver and time zone responsibility to one of the Spring's implementation of LocaleContextResolver:

public class AcceptHeaderLocaleTzCompositeResolver
                                            implements LocaleContextResolver {
    private LocaleContextResolver localeContextResolver;
    private AcceptHeaderLocaleResolver acceptHeaderLocaleResolver;

    public AcceptHeaderLocaleTzCompositeResolver (
                                      LocaleContextResolver localeContextResolver) {
        this.localeContextResolver = localeContextResolver;
        acceptHeaderLocaleResolver = new AcceptHeaderLocaleResolver();
        acceptHeaderLocaleResolver.setDefaultLocale(Locale.getDefault());
    }

    @Override
    public LocaleContext resolveLocaleContext (HttpServletRequest request) {
        return localeContextResolver.resolveLocaleContext(request);
    }

    @Override
    public void setLocaleContext (HttpServletRequest request,
                                  HttpServletResponse response,
                                  LocaleContext localeContext) {
        localeContextResolver.setLocaleContext(request, response, localeContext);

    }

    @Override
    public Locale resolveLocale (HttpServletRequest request) {
        return acceptHeaderLocaleResolver.resolveLocale(request);
    }

    @Override
    public void setLocale (HttpServletRequest request,
                                  HttpServletResponse response, Locale locale) {
        acceptHeaderLocaleResolver.setLocale(request, response, locale);

    }
}



Creating a custom HandlerInterceptor

A custom HandlerInterceptor implementation will intercept each request and will try to obtain TimeZone instance. If it is null then forward the request to an handler

public class TzRedirectInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle (HttpServletRequest request,
                              HttpServletResponse response,
                              Object handler) throws Exception {

        TimeZone tz = RequestContextUtils.getTimeZone(request);

        if (tz == null) {
            System.out.println("Forwarding to js to get timezone offset");
            request.setAttribute("requestedUrl", request.getRequestURI());
            RequestDispatcher dispatcher =
                                request.getRequestDispatcher("/tzHandler");
            dispatcher.forward(request, response);
            return false;
        }

        return true;
    }
}

RequestContextUtils is a helper class. Above call gets the time zone from the underlying ContextLocalResolver.



The handler method with '/tzHandler' mapping:

The above interceptor forwarded the request to this handler.

  @RequestMapping("/tzHandler")
    public String handle () {
        return "tzJsPage";
    }


tzJsPage.jsp

This contains javascript to obtain client's time zone offset. This page directly redirects to another URI /tzValueHandler without any user interaction:

<%@ page language="java"
    contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>

<html>
 <body>
    <form method="post" id="tzForm" action="/tzValueHandler">
      <input id="tzInput" type="hidden" name="timeZoneOffset"><br>
      <input type="hidden" name="requestedUrl" value="${requestedUrl}">
    </form>

    <script>
        var date = new Date()
        var offSet = date.getTimezoneOffset()
        document.getElementById("tzInput").value = offSet;
        document.getElementById("tzForm").submit();
    </script>
 </body>
</html>


The handler method with '/tzValueHandler' mapping

This handler sets the time zone instance to the underlying LocaleContextResolver:

    @RequestMapping(value = "/tzValueHandler", method = RequestMethod.POST)
    public String handleTzValue (
              Locale locale, HttpServletRequest req,
              HttpServletResponse res,
              @RequestParam("requestedUrl") String requestedUrl,
              @RequestParam("timeZoneOffset") int timeZoneOffset) {


        ZoneOffset zoneOffset =
                  ZoneOffset.ofTotalSeconds(-timeZoneOffset * 60);

        TimeZone timeZone = TimeZone.getTimeZone(zoneOffset);

        LocaleContextResolver localeResolver =
                  (LocaleContextResolver) RequestContextUtils.getLocaleResolver(r);

        localeResolver.setLocaleContext(req, res,
                                        new SimpleTimeZoneAwareLocaleContext(
                                                  locale, timeZone));

        return "redirect:" + requestedUrl;

    }


Handler method for testing

@RequestMapping("/")
@ResponseBody
public String testHandler (Locale clientLocale, ZoneId clientZoneId) {

   ZoneOffset serverZoneOffset = ZoneOffset.ofTotalSeconds(
             TimeZone.getDefault().getRawOffset() / 1000);

   return String.format("client timeZone: %s" +
                                  "<br> " +
                                  "server timeZone: %s" +
                                  "<br>" +
                                  " locale: %s%n",
                        clientZoneId.normalized().getId(),
                        serverZoneOffset.getId(),
                        clientLocale);
}


The Spring boot main class:

@SpringBootApplication
public class TimeZoneExampleMain {
   public static void main (String[] args) {
       SpringApplication.run(TimeZoneExampleMain.class, args);
   }

   @Bean
   LocaleContextResolver localeResolver () {
       SessionLocaleResolver l = new SessionLocaleResolver();
       AcceptHeaderLocaleTzCompositeResolver r = new
                 AcceptHeaderLocaleTzCompositeResolver(l);
       return r;
   }

   @Bean
   public WebMvcConfigurer configurer () {
       return new WebMvcConfigurerAdapter() {
           @Override
           public void addInterceptors (InterceptorRegistry registry) {
               TzRedirectInterceptor interceptor = new TzRedirectInterceptor();
               InterceptorRegistration i = registry.addInterceptor(interceptor);
               i.excludePathPatterns("/tzHandler", "/tzValueHandler");
           }
       };
   }
}


Output

Run boot plugin

 mvn spring-boot:run

Since in development environment both clients and server are most likely to be running on the same computer, we are going to use FireFox browser which allows to change browser time zone from an environmental variable. Run cmd.exe:

C:\Program Files (x86)\Mozilla Firefox>set TZ=GMT2

C:\Program Files (x86)\Mozilla Firefox>firefox.exe

C:\Program Files (x86)\Mozilla Firefox>

In the console where we ran boot plugin, a line will be printed:
'Forwarding to js to get timezone offset'
This line is printed by our interceptor. Refreshing the same page multiple times won't print the same line again, because for further access, time zone will be retrieved from the HttpSession and request won't be forwarded to the JavaScript logic.




Using CookieLocaleResolver and FixedLocaleResolver

In above example we used SessionLocaleResolver (as a LocaleContextResolver delegate, specific to time zone handling) with our AcceptHeaderLocaleTzCompositeResolver. The server side will remember the time zone set by JavaScript logic till the end of the session. If we want it to be remembered longer or shorter than session life time then we should use CookieLocaleResolver for time zone handling.

In case if we want to use a fixed time zone other than system time zone, then we should use FixedLocaleResolver with our AcceptHeaderLocaleTzCompositeResolver, in that case we won't need our custom TzRedirectInterceptor and JavaScript logic. The fixed time zone value will be set at the time of registration in the main class.

With all options, we just mentioned, Locale selection strategy will remain the same in our example i.e. AcceptHeaderLocaleResolver. If we want to change that too, we won't need a composite LocaleContextResolver. A single LocaleContextResolver will do the job unless we want to apply different strategies for each locale and timezone. For locale handling, with a resolver other than AcceptHeaderLocaleResolver, we have to provide a custom locale selection as well, what we saw in this and this examples. The time zone can also be selected by a user interface instead of using JavaScript Date API.




Example Project


Dependencies and Technologies Used :
  • spring-boot-starter-web 1.4.3.RELEASE: Starter for building web, including RESTful, applications using Spring MVC. Uses Tomcat as the default embedded container.
    Corresponding Spring version: 4.3.5.RELEASE
  • tomcat-embed-jasper 8.5.6: Core Tomcat implementation.
  • JDK 1.8
  • Maven 3.3.9

Client Time Zone Example Select All Download
  • spring-mvc-timezone
    • src
      • main
        • java
          • com
            • logicbig
              • example
        • resources
        • webapp
          • WEB-INF
            • views


See Also

Spring core i18n
Spring MVC i18n
SessionLocaleResolver example
CookieLocaleResolver example
FixedLocaleResolver example
Implementing HandlerInterceptor
Using forward: prefix
Spring boot tutorials