There is just one free timezone REST service on the web: http://worldtimeapi.org/. Here you can query the date, time, UTC-offset, and whether daylight saving time (DST) currently is active, for any location. Try e.g. Europe/Vienna.
This article is about a small Java 11 Servlet 4.0.1 microservice
that provides a REST/JSON interface (API), and a JSP-page
that calls that REST interface via the new AJAX fetch()
function.
Subject is displaying Java
timezones
in a web browser (they are not yet available in JavaScript).
Occasion was that the new way to write user-interfaces is meant to be
'calling REST services
from different devices like browsers, phones, desktop-applications'.
That reminds me of the 1990ies when we had to implement desktop user-interfaces
for WINDOWS, UNIX and MACINTOSH systems separately ...
looks like we are back there, despite of the Java write-once-run-everywhere promise
that also included user-interfaces.
However, let's try it.
Introduction
This is a big article. Here is a screenshot of the resulting web UI, called from Europe/Vienna timezone, filtered for zones containing "Singapore":
Purpose was to have an overview of the different timezones offsets (GMT), seeing the time differences, seeing whether daylight saving time (DST) is used by a zone, and whether it is in action at a chosen date. Here is another screenshot, showing the result-treetable when calling the servlet without flter:
Writing this web app I learnt a lot about the
new way to write servlets,
JSP, the new java.time
package and its old counterparts,
and how date/time is handled in web browsers and the according input fields.
It was interesting to configure and build the servlet with Maven and Eclipse,
and then run it with two different web-servers:
Jetty 10 (from command line) and Tomcat 9 (from Eclipse).
Minimal Servlet 4.0
The maven-archetype-webapp
is nice for setting up a
minimal servlet,
but it generates more sources than needed.
WEB-INF/web.xml
can be omitted when using the @WebServlet
annotation
(on the Java class extending HttpServlety
),
assumed that the servlet-container can process such annotations.
Tomcat 9 and Jetty 10 can, and both support JSP
translation without additional plugins or dependencies.
That means you can use Java code inside a JSP page
that contains also HTML, CSS and JavaScript.
A babylonical mix, not even Eclipse always succeeds in syntax-highlighting that correctly!
Following is the file and folder structure of the timezones
servlet Maven project:
timezones
src
main
webapp
java
fri
servlets
timezones
You find the implementations in the following.
Here is the Maven pom.xml
:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>fri.servlets</groupId> <artifactId>timezones</artifactId> <!-- is also default servlet contextPath --> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <properties> <!-- fix missing web.xml, needed when using servlet annotations--> <failOnMissingWebXml>false</failOnMissingWebXml> <!-- fix Eclipse bug "Dynamic Web Module 4.0 requires Java 1.8 or newer" --> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.json</groupId> <artifactId>json</artifactId> <version>20211205</version> </dependency> </dependencies> </project>
So a minimal servlet depends on the javax.servlet-api
library and nothing else.
I use the org.json
artifact for creating JSON in the REST interface,
it is not needed for a servlet.
If you want to use Jetty as test server,
then append following after <dependencies>
: (click to expand)
<build> <plugins> <!-- Jetty web server, optional for command-line 'mvn jetty:run'. Version 11.0.7 does not work with servlet 4.0, thus it's version 10. --> <plugin> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-maven-plugin</artifactId> <version>10.0.7</version> <configuration> <scanIntervalSeconds>2</scanIntervalSeconds> <webApp> <contextPath>/${artifactId}</contextPath> </webApp> <httpConnector> <port>8080</port> </httpConnector> </configuration> </plugin> </plugins> </build>
With this plugin you will be able to run Jetty by opening a command-terminal,
changing to the project folder, and entering the command mvn jetty:run
there.
Mind the <contextPath>/${artifactId}</contextPath>
element,
this makes it possible to use the same servlet context path as Tomcat,
without duplicating code.
Servlet Implementation
Two sources make up the servlet facade:
→ index.jsp is the user interface and base page, providing manual input of query parameters. On action it will call the REST interface to fetch timezones according to the parameters. This UI will show when called with an URL like
http://localhost:8080/timezones
→ TimezonesServlet.java represents the REST/JSON interface, callable by URLs like
http://localhost:8080/timezones/zones?day=2021-12-31&time=23:59&zone=Europe/Vienna&filter=Singapore
So timezones
is the servlet context path, leading to the UI,
and zones
is the REST path part, followed by runtime-estimated URL parameters, leading to JSON.
Only the HTTP GET
method is supported.
As there is no PUT and POST, there is also no HTML form
element in index.jsp
.
This is a read-only servlet, without any log-in authentication.
Client: index.jsp
The user interface.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | <!DOCTYPE html> <html> <!-- Provides manual parameter input, showing on URL http://localhost:8080/timezones --> <head> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta charset="UTF-8" /> <title>Java Timezones Servlet</title> <script src="ZonesTreeBuilder.js"></script> <!-- contains generateHtml() --> <!-- functions related to input fields and AJAX-calls --> <script> const fetchTimezones = function() { /* clear result element */ timezonesDisplay.innerHTML = ""; if ( ! dayField.value ) /* happens when clearing date field */ return; /* get user input values and build a REST-URL by prepending "zones" */ const requestUrl = window.location.href+ "zones"+ "?day="+encodeURIComponent(dayField.value)+ "&time="+encodeURIComponent(timeField.value)+ "&zone="+encodeURIComponent(zoneField.value)+ "&filter="+encodeURIComponent(filterField.value); const expandRoots = (filterField.value != ""); /* make an AJAX-call to REST server with parameterized URL */ fetch(requestUrl) .then(response => checkFetchResponse(response)) .then(json => timezonesDisplay.innerHTML = generateHtml(json, expandRoots)) .catch(error => alert(error)); }; const checkFetchResponse = function(response) { if ( ! response.ok ) throw new Error("Network error, status: "+response.status+", statusText: "+response.statusText); const contentType = response.headers.get("content-type"); if ( ! contentType.includes("application/json") ) throw new Error("Response should be JSON!"); return response.json(); }; /* Expand/Collapse tree button callbacks */ const expandAll = function() { expandOrCollapse(true); }; const collapseAll = function() { expandOrCollapse(false); }; const expandOrCollapse = function(expand) { for (const detailsElement of timezonesDisplay.querySelectorAll("details")) detailsElement.open = expand; }; </script> </head> <body> <% pageContext.setAttribute("zoneIds", java.time.ZoneId.getAvailableZoneIds() .stream() .filter(id -> id.contains("/") && id.startsWith("Etc/") == false && id.startsWith("SystemV/") == false) .sorted() .map(id -> id.equals(java.time.ZoneId.systemDefault().getId()) ? "<option selected>"+id+"</option>" : "<option>"+id+"</option>") .collect(java.util.stream.Collectors.joining()) ); %> <h2>Welcome to Java <%=System.getProperty("java.version")%> Timezones!</h2> <!-- REST parameter inputs --> <table style="border: 1px solid gray; border-radius: 0.4em; padding: 0.6em;"> <tr> <td>* Day</td> <td><input id="day" type="date" onchange="fetchTimezones();"/></td> <td>Time</td> <td><input id="time" type="time"/></td> </tr> <tr> <td>Zone</td> <td colspan="3"><select id="zone" onchange="fetchTimezones();">${zoneIds}</select></td> </tr> <tr> <td>Filter</td> <td colspan="2"><input id="filter" type="text" onchange="fetchTimezones();" style="width: 8.5em;"/></td> <td><input id="fetch" type="button" value="Fetch" onclick="fetchTimezones();"/></td> </tr> </table> <!-- glossary --> <table style="padding: 0.6em;"> <tr><td>DST</td><td>→ Daylight Saving Time</td></tr> <tr><td>GMT</td><td>→ Greenwich Mean Time</td></tr> </table> <!-- tree control buttons --> <div> <input type="button" value="Expand All" onclick="expandAll();"/> <input type="button" value="Collapse All" onclick="collapseAll();"/> </div> <div id="timezones"> <!-- tree output area --> </div> <!-- initialization, executed once on page-load --> <script> /* get global fields into constants */ const dayField = document.getElementById("day"); const timeField = document.getElementById("time"); const zoneField = document.getElementById("zone"); const filterField = document.getElementById("filter"); const timezonesDisplay = document.getElementById("timezones"); /* put current day and time into "day" and "time" fields */ const dt = new Date(); const twoDigits = (value) => (value < 10 ? "0" : "")+value; dayField.value = dt.getFullYear()+"-"+twoDigits(dt.getMonth() + 1)+"-"+twoDigits(dt.getDate()); timeField.value = twoDigits(dt.getHours())+":"+twoDigits(dt.getMinutes()); /* add change-listener to disable fetch-button when no day is chosen */ dayField.addEventListener("change", function() { const fetchButton = document.getElementById("fetch"); fetchButton.disabled = dayField.value ? false : true; }); </script> </body> </html> |
Now this is big. It consists of several parts that I will describe step by step.
ZoneTreeBuilder.js in Head
Referenced in line 13. Here is a screenshot of what it can produce:
This timezones tree is made up by HTML
DETAIL
elements.
Here is the JavaScript ZonesTreeBuilder.js
implementation,
turning JSON into HTML. (Click to expand)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | /** * Build a DETAILS tree HTML text from server's time zones JSON. * @param zonesJson the JSON object that carries time zone data. * @param expandRoots when given and true, tree roots will be expanded. * @return HTML text that contains time zones informations. */ const generateHtml = function(zonesJson, expandRoots) { /** * Builds headline, expand-buttons and tree, including CSS style. * @param htmlPrintln function that prints HTML into a buffer. */ const outputTree = function(htmlPrintln) { const printTreeStyles = function() { htmlPrintln("<style>"); htmlPrintln(" details.tree {"); htmlPrintln(" padding-left: 1em;"); htmlPrintln(" font-weight: bold;"); htmlPrintln(" }"); htmlPrintln(" div.tree {"); htmlPrintln(" padding-left: 2.06em;"); htmlPrintln(" font-style: italic;"); htmlPrintln(" font-weight: bold;"); htmlPrintln(" }"); htmlPrintln(" details.tree details.tree, details.tree div.tree {"); htmlPrintln(" font-weight: normal;"); htmlPrintln(" }"); /* render zone-IDs with fixed width so that DST is aligned like in a table */ htmlPrintln(" div.tree span.zoneIdSpace {"); htmlPrintln(" display: inline-block;"); htmlPrintln(" width: 19em;"); /* fits also "America/North_Dakota/New_Salem" */ htmlPrintln(" }"); htmlPrintln("</style>"); }; const printTree = function() { htmlPrintln( "<p><b>"+zonesJson.queryDay+"</b> → "+ zonesJson.offsets.length+" zone offsets.</p>"); for (const offset of zonesJson.offsets) { htmlPrintln( "<details "+(expandRoots ? "open" : "")+" class='tree'><summary>"+ offset.offsetHours+" hours off GMT: "+ offset.zonedDateTime+ "</summary>"); for (const displayName of offset.displayNames) { htmlPrintln( "<details open class='tree'><summary>"+ displayName.displayName+ "</summary>"); for (const zone of displayName.zones) { htmlPrintln( "<div class='tree'>"+ "<span class='zoneIdSpace'>"+zone.zoneId+"</span>"+ (zone.usesDaylightSavingTime ? printDaylightSavingTimes(zone) : "")+ "</div>"); } /* end zones */ htmlPrintln("</details>"); } /* end displayNames */ htmlPrintln("</details>"); } /* end offsets */ }; const printDaylightSavingTimes = function(zone) { var s = "Uses DST, "+zone.previousTransition+"^"+zone.nextTransition; if (zone.inDaylightSavingTime) s += " ☀ In DST, "+zone.standardOffsetHours+"+"+zone.correctionHours; return s; }; /* enclosing DIV, style prevents inherited text-align destroying tree indents */ htmlPrintln("<div style='text-align: initial;'>"); printTreeStyles(); printTree(); htmlPrintln("</div>"); }; /** Call outputTree() and return outputText */ var outputText; outputTree(nextLine => outputText = (outputText ? outputText+"\n" : "")+nextLine); return outputText; }; |
On line 88 - 90 you see how generateHtml()
works:
it provides a lambda that writes HTML text into a result buffer and
calls outputTree()
with it.
Then it returns the result buffer.
The outputTree()
function starting on line 13 contains
several sub-functions. On line 80 - 83 you see how it works.
It writes an enclosing DIV into the given HTML-writer,
then calls all sub-functions, then closes the DIV.
The printTreeStyles()
function on line 15 generates CSS necessary
to turn the DETAIL
elements into an indented tree.
The CSS is nested in a local <style>
element.
The printTree()
function on line 39 finally prints timezone contents.
It iterates through the JSON model with three nested loops, generating HTML accordingly.
The innermost loop calls printDaylightSavingTimes()
for rendering zone informations
concerning daylight saving times.
Each loop closes its open DETAIL
at end.
So, on JavaScript side, other than in Java, there is no model visitor,
instead the model is traversed directly by implementing loops.
Even when there was a visitor, it would have to be identical with the Java visitor,
which is the big code-duplication problem here.
This JS code knows the nature of the model built by the Java server
(ZoneTreeModel.java
and ZoneTreeView.java
).
Both must use the same tree structure and property names.
This may be the biggest weakness of REST apps!
More Script Functions in Head
Line 16 to 64.
The fetchTimezones()
function calls the REST interface.
Line 21 would deny when no day was chosen.
You see the URL built from line 25 to 30, the REST path part on line 26.
Four parameters go into the URL:
day
, time
, zone
and filter
.
Line 35 uses the built-in JavaScript method fetch()
to read timezones from the Java server.
This works with
Promises.
The checkFetchResponse()
function on line 41 asserts the result and turns it into JSON.
The lambda on line 37 then turns the JSON into HTML, by calling the generateHtml()
function in ZoneTreeBuilder.js
, and sets it into the timezonesDisplay
HTML element.
Result will be an expandable tree of timezone offsets, display names, and zones with DST infos.
On line 52 - 63 there are functions that can fully expand or collapse the timezones tree.
They are called by the according input
buttons.
Building Zone Choice by JSP-Java
Line 69 to 81.
The <% %>
match on line 69 is a JSP tag where you can insert Java code.
This is evaluated before delivering HTML to the browser.
I used it to build together the choice of all zone-names available in Java.
The default zone is selected through line 76 and 77.
The JSP page-context attribute zoneIds
contains a list of HTML <option>
elements afterwards.
In line 95 you find the usage of the zoneIds
attribute.
It is simply expanded into an HTML select
element.
Parameter Inputs
Line 86 to 102.
The day is a mandatory parameter, although the REST servlet would default to current day when not given.
The time chooser is a browser-specific thing, Firefox renders it with AM/PM for English locale.
The zone choice is also part of the parameter panel. It allows to virtually move to the chosen timezone before calculating offsets, times and DST.
Last not least you can restrict the number of displayed timezones by a filter pattern
that is evaluated by zonId.contains(filter)
on the server.
That filter is a good example that filtering makes sense also on server side,
because it minimizes the network load of transferring HTML elements.
The "Expand All" and "Collapse All" buttons on line 111 to 114 control the timezones result tree.
Line 116 and 117 finally contain the timezones result panel, intially empty.
Initialization Script on Bottom
Line 120 to 139.
This is executed when all HTML page elements were set up. Global variables reference the parameter input fields for later use (line 122 - 126).
Line 129 - 132 put the current day and time into the according input fields.
This was not easy to implement, because JS Date
does not provide
the right strings that we can set into such fields.
Thus I tried to build them through toISOString()
, but this
displayed the UTC time (without GMT offset) instead of the local time.
Further an input field of type time (and also date) always requires two-digit numbers.
This is the reason for the twoDigit
lambda on line 130 that
is then called when building together the local date and time in ISO-format:
"2022-01-02" and "08:01". The forms "2022-1-2" and "8:1" won't be accepted.
Line 135 - 138 install a change-listener onto the day-field, so that the "Fetch" button will be disabled when no day is chosen.
User interfaces always are complex. This JSP page contains several technologies and programming languages. For sure it was the most time-consuming part of this project. What now follows is the easier part: Java only!
Server: TimezonesServlet.java
I left out the package definition fri.servlets.timezones
and all imports,
coming from java.io
, java.time
, java.util.function
,
javax.servlet
, javax.servlet.annotation
and javax.servlet.http
.
The @WebServlet
annotation on line 1 is a hint for the
servlet container (web server) that this class is a servlet class.
This replaces the servlet definitions in web.xml
, you don't need that file any more.
The ("/zones")
path tells the container for which URL the servlet is to call,
relative to the servlet context path timezones
that is the Maven artifact
identifier in pom.xml
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | @WebServlet("/zones") public class TimezonesServlet extends HttpServlet { @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { final String day = request.getParameter("day"); final String time = request.getParameter("time"); if (day != null || time != null) { // coming from http://localhost:8080/timezones/zones?day=2012-12-30&time=15:30 final String zone = request.getParameter("zone"); final String filter = request.getParameter("filter"); final String responseText = dispatch(day, time, zone, filter); response.setContentType("application/json"); final PrintWriter output = response.getWriter(); output.println(responseText); output.close(); } else { // no day or time given, back to ground at http:///localhost:8080/timezones/ getServletContext().getRequestDispatcher("/index.jsp").forward(request, response); } } private String dispatch(String day, String time, String zone, String filter) { if (day == null || day.length() <= 0) day = LocalDate.now().toString(); if (time == null || time.length() <= 0) time = LocalTime.now().toString(); final String theZoneId = (zone != null && zone.length() > 0) ? zone : ZoneId.systemDefault().getId(); final ZoneId theZone = ZoneId.of(theZoneId); final String theFilter = (filter != null && filter.length() > 0) ? filter.toLowerCase() : null; final Predicate<String> zoneIdFilter = (theFilter != null) ? id -> id.toLowerCase().contains(theFilter) : null; final long millis = LocalDate.parse(day) .atTime(LocalTime.parse(time)) .atZone(theZone) .toInstant() .toEpochMilli(); final JSONObject jsonObject = new ZonesTreeView().buildJson(zoneIdFilter, millis, theZone); return jsonObject.toString(); } } |
To serve HTTP GET
calls, I override the HttpServlet.doGet()
method.
From line 8 to 14 you see the same URL parameter names as in index.jsp
in function fetchTimezones()
. In case neither day nor time was
provided in the request parameters, the call is redirected to the index.jsp
UI.
Else the dispatch()
method is called, and the returned JSON is then
printed into the response (line 17 - 20).
The dispatch()
method evaluates its parameters and
provides defaults when needed (current day, current time, default zone, line 28 - 37).
From line 39 to 47, parameters for ZonesTreeView
are built together,
which is then called on line 49. The resulting JSON object is returned, serialized as text, in line 50.
That's all for the servlet class. The remaining part is business logic.
Model
The structure of the inner classes outlines the tree structure, which consists of three levels:
- timezone offsets (in hours)
- display-names (grouping of zones)
- zones, carrying DST (daylight saving times)
Following is a typical Java model,
outlining timezones as defined in the new java.time
API.
It provides a visitor to free callers from the burden of duplicating model traversals.
(Click to expand)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | public class ZonesTreeModel { public class Offset { public class DisplayName { public class Zone { public final ZoneId zone; public Zone(ZoneId zone) { this.zone = zone; } public int correctionMillis() { return offset - standardOffsetMillis(); // offset taken from outer instance } public int standardOffsetMillis() { return zone.getRules().getStandardOffset(instant).getTotalSeconds() * 1000; // instant taken from outer instance } public boolean inDaylightSavingTime() { return zone.getRules().isDaylightSavings(instant); } public boolean usesDaylightSavingTime() { return zone.getRules().nextTransition(instant) != null; } public LocalDate[] daylightSavingTimeTransitions() { if (usesDaylightSavingTime() == false) return null; final ZoneRules rules = zone.getRules(); final ZoneOffsetTransition nextTransition = rules.nextTransition(instant); final LocalDateTime next = nextTransition.getDateTimeBefore(); final ZoneOffsetTransition previousTransition = rules.previousTransition(instant); final LocalDateTime previous = previousTransition.getDateTimeAfter(); return new LocalDate[] { previous.toLocalDate(), next.toLocalDate() }; } public void accept(ZonesTreeModel.Visitor visitor) { visitor.visitZone(this); } } public final String displayName; private final List<Zone> zones = new ArrayList<>(); public DisplayName(String displayName) { this.displayName = displayName; } void add(Zone zone) { zones.add(zone); } public List<Zone> zones() { return Collections.unmodifiableList(zones); } public void accept(ZonesTreeModel.Visitor visitor) { visitor.visitDisplayName(this); for (Zone zone : zones) zone.accept(visitor); } } public final int offset; public final ZonedDateTime zonedDateTime; private final List<DisplayName> displayNames = new ArrayList<>(); public Offset(int offset, ZoneId zone) { this.offset = offset; this.zonedDateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(zonedMillisSince1970 + offset), zone); } void add(DisplayName displayName) { displayNames.add(displayName); } public List<DisplayName> displayNames() { return Collections.unmodifiableList(displayNames); } public void accept(ZonesTreeModel.Visitor visitor) { visitor.visitOffset(this); for (DisplayName displayName : displayNames) displayName.accept(visitor); } } private final long zonedMillisSince1970; private final Instant instant; private final List<Offset> offsets = new ArrayList<>(); public ZonesTreeModel(long millisSince1970, ZoneId zone) { // calculate zoned millis from UTC millis this.zonedMillisSince1970 = millisSince1970 - zone.getRules().getOffset(Instant.ofEpochMilli(millisSince1970)).getTotalSeconds() * 1000; this.instant = Instant.ofEpochMilli(zonedMillisSince1970); } void add(Offset offset) { offsets.add(offset); } public List<Offset> offsets() { return Collections.unmodifiableList(offsets); } public void accept(ZonesTreeModel.Visitor visitor) { visitor.visitZoneTree(this); for (Offset offset : offsets) offset.accept(visitor); } /** * Visitor implementations don't need to duplicate structural loops. */ public interface Visitor { void visitZoneTree(ZonesTreeModel tree); void visitOffset(Offset offset); void visitDisplayName(DisplayName displayName); void visitZone(Zone zone); } } |
On line 15 - 38 you can see how the model refers to Java timezones for calculating offsets and retrieving DST and other information.
Line 115 - 121 define the Visitor
interface for viewing the model.
The model implements visiting by the accept(Visitor)
method on every tree level.
Builder
The builder creates a ZonesTreeModel
from Java timezones.
(Click to expand)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | public class ZonesTreeBuilder { public ZonesTreeModel buildTree(Predicate<String> zoneIdFilter, long millisSince1970, ZoneId zoneId) { final String[] zoneIds = ZoneId.getAvailableZoneIds() .stream() .filter(zoneIdFilter) .collect(Collectors.toList()) .toArray(String[]::new); final ZonesTreeModel tree = new ZonesTreeModel(millisSince1970, zoneId); buildOffset2ListOfTimeZonesMap(millisSince1970, zoneIds) .entrySet() .stream() .sorted((e1, e2) -> e2.getKey() - e1.getKey()) // positive offsets first, e.g. Pacific/Tongatapu .forEach(offsetEntry -> { final Offset offset = tree.new Offset(offsetEntry.getKey(), zoneId); tree.add(offset); buildDisplayName2ListOfTimeZonesMap(offsetEntry.getValue()) .entrySet() .stream() .sorted((e1, e2) -> e1.getKey().compareTo(e2.getKey())) .forEach(displayEntry -> { final DisplayName displayName = offset.new DisplayName(displayEntry.getKey()); offset.add(displayName); displayEntry.getValue() .stream() .sorted((z1, z2) -> z1.getId().compareTo(z2.getId())) .forEach(timeZone -> { final Zone zone = displayName.new Zone(timeZone); displayName.add(zone); }); }); }); return tree; } private Map<Integer,List<ZoneId>> buildOffset2ListOfTimeZonesMap(long millisSince1970, String[] zoneIds) { final Instant instant = Instant.ofEpochMilli(millisSince1970); return Arrays.stream(zoneIds) .map(zoneId -> ZoneId.of(zoneId)) .collect( Collectors.groupingBy( zone -> zone.getRules().getOffset(instant).getTotalSeconds() * 1000, Collectors.toList() ) ); } private Map<String,List<ZoneId>> buildDisplayName2ListOfTimeZonesMap(List<ZoneId> zones) { return zones.stream() .collect( Collectors.groupingBy( zone -> zone.getDisplayName(TextStyle.FULL, Locale.getDefault()), Collectors.toList() ) ); } } |
Everything is done through functional stream code. Too complicated to explain :-)
View
To provide different views onto the model (JSON, CSV, XML, ...),
the ZonesTreeView
uses the model visitor interface.
For now there is just one view, converting the model to JSON objects.
(Click to expand)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | public class ZonesTreeView { public JSONObject buildJson(Predicate<String> zoneIdFilter, long now, ZoneId zoneId) { if (zoneIdFilter == null) zoneIdFilter = id -> id.contains("/") && id.startsWith("Etc/") == false && id.startsWith("SystemV/") == false; final JSONObject root = new JSONObject(); root.put("queryDay", ""+ZonedDateTime.ofInstant(Instant.ofEpochMilli(now), zoneId)); final ZonesTreeModel tree = new ZonesTreeBuilder().buildTree(zoneIdFilter, now, zoneId); tree.accept(new ZonesTreeModel.Visitor() { private JSONArray offsets; private JSONArray displayNames; private JSONArray zones; @Override public void visitZoneTree(ZonesTreeModel tree) { root.put("offsets", offsets = new JSONArray()); } @Override public void visitOffset(Offset offset) { final JSONObject offsetJsonObject = new JSONObject(); offsetJsonObject.put("offsetHours", ""+toHours(offset.offset)); offsetJsonObject.put("zonedDateTime", ""+DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").format(offset.zonedDateTime)); displayNames = new JSONArray(); offsetJsonObject.put("displayNames", displayNames); offsets.put(offsetJsonObject); } @Override public void visitDisplayName(DisplayName displayName) { final JSONObject displayNameJsonObject = new JSONObject(); displayNameJsonObject.put("displayName", displayName.displayName); zones = new JSONArray(); displayNameJsonObject.put("zones", zones); displayNames.put(displayNameJsonObject); } @Override public void visitZone(Zone zone) { final JSONObject zoneJsonObject = new JSONObject(); zoneJsonObject.put("zoneId", zone.zone.getId()); zoneJsonObject.put("usesDaylightSavingTime", zone.usesDaylightSavingTime()); if (zone.usesDaylightSavingTime()) { final LocalDate[] transitions = zone.daylightSavingTimeTransitions(); if (transitions != null) { zoneJsonObject.put("previousTransition", ""+transitions[0]); zoneJsonObject.put("nextTransition", ""+transitions[1]); } zoneJsonObject.put("inDaylightSavingTime", zone.inDaylightSavingTime()); if (zone.inDaylightSavingTime()) { zoneJsonObject.put("standardOffsetHours", ""+toHours(zone.standardOffsetMillis())); zoneJsonObject.put("correctionHours", ""+toHours(zone.correctionMillis())); } } zones.put(zoneJsonObject); } private String toHours(long millis) { final float hours = (float) millis / (float) 3600000; final String hourString = ""+hours; final String useless = ".0"; return hourString.endsWith(useless) ? hourString.substring(0, hourString.length() - useless.length()) : hourString; } }); return root; } } |
Mind that this source uses the same property names as the ZoneTreeBuilder.js
JavaScript!
Resume
It took me days to implement this servlet. I struggled with Eclipse project facets, the servlet containers (web servers), different library versions not accepting others, obscurity of servlet URLs in different containers, web browser problems with date, time and the according HTML input elements, programming language and framework gotchas and the necessity to know them all well.
I would have liked to use the application sometimes for presenting the beauty of Java timezones, but how can I use such an application? It is "dynamic web content", so I would have to pay a VPS hosting provider (virtual private server), or a shared container hosting provider, to expose it on the Internet. Or I could start the server on my private laptop, then connect to it with my browser, but can anyone else see that?
Servlets belong to the Internet, it doesn't make sense to implement servlets running on private machines. So I will implement a timezones Swing desktop application for private use only. This is better than having to start up a web server and connect to it with a browser each time you want to look at time zones.
The development of web applications is not over with REST. What is coming is user interfaces that massively duplicate code (model structure and property names). Remember: UIs are the most expensive part of an application! A change in a server model may kill many UI implementations then. After DLL hell, JAR hell, and any other kind of dependency hell, the REST-API hell is in action now.
Keine Kommentare:
Kommentar veröffentlichen