commit b040d1a28de7e65b7f16c3d529aa205d750d6d3c Author: iwakeh iwakeh@torproject.org Date: Mon Dec 11 13:58:03 2017 +0000
Introduce a date class encapsulating all calculations related to dates.
Also use LocalDate for date parsing; turn tests into parametrized tests and add tests for checking validity of parameters. --- .../metrics/exonerator/ExoneraTorDate.java | 47 +++++++++ .../metrics/exonerator/ExoneraTorServlet.java | 110 +++++++-------------- .../metrics/exonerator/ExoneraTorDateTest.java | 80 +++++++++++++++ .../metrics/exonerator/ExoneraTorServletTest.java | 29 ------ 4 files changed, 163 insertions(+), 103 deletions(-)
diff --git a/src/main/java/org/torproject/metrics/exonerator/ExoneraTorDate.java b/src/main/java/org/torproject/metrics/exonerator/ExoneraTorDate.java new file mode 100644 index 0000000..723ffdd --- /dev/null +++ b/src/main/java/org/torproject/metrics/exonerator/ExoneraTorDate.java @@ -0,0 +1,47 @@ +/* Copyright 2017 The Tor Project + * See LICENSE for licensing information */ + +package org.torproject.metrics.exonerator; + +import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE; + +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.time.format.DateTimeParseException; + +class ExoneraTorDate { + + public static final ExoneraTorDate INVALID = new ExoneraTorDate(""); + private static final int DATELENGTH = "yyyy-mm-dd".length(); + + final boolean empty; + final boolean valid; + final String asString; + final LocalDate date; + final String asRequested; + final boolean tooRecent; + + ExoneraTorDate(String parameter) { + this.asRequested = parameter; + this.empty = null == parameter || parameter.trim().isEmpty(); + this.date = empty ? null : parseDatestamp(parameter); + this.valid = null != date; + this.asString = valid ? date.format(ISO_LOCAL_DATE) : ""; + this.tooRecent = valid + && date.isAfter(LocalDate.now(ZoneOffset.UTC).minusDays(2)); + } + + private static LocalDate parseDatestamp(String datestamp) { + String trimmedDatestamp = datestamp.replaceAll("\s", ""); + if (trimmedDatestamp.length() >= DATELENGTH) { + try { + return LocalDate + .parse(trimmedDatestamp.substring(0, DATELENGTH), ISO_LOCAL_DATE); + } catch (DateTimeParseException e) { + return null; + } + } + return null; + } + +} diff --git a/src/main/java/org/torproject/metrics/exonerator/ExoneraTorServlet.java b/src/main/java/org/torproject/metrics/exonerator/ExoneraTorServlet.java index 1ee857b..8464f50 100644 --- a/src/main/java/org/torproject/metrics/exonerator/ExoneraTorServlet.java +++ b/src/main/java/org/torproject/metrics/exonerator/ExoneraTorServlet.java @@ -3,8 +3,6 @@
package org.torproject.metrics.exonerator;
-import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE; - import org.apache.commons.lang3.StringEscapeUtils;
import org.slf4j.Logger; @@ -15,10 +13,6 @@ import java.io.InputStreamReader; import java.io.PrintWriter; import java.io.StringWriter; import java.net.URL; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.time.LocalDate; -import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -26,7 +20,6 @@ import java.util.Locale; import java.util.Map; import java.util.ResourceBundle; import java.util.SortedMap; -import java.util.TimeZone; import java.util.TreeMap; import java.util.regex.Pattern;
@@ -77,11 +70,8 @@ public class ExoneraTorServlet extends HttpServlet { final boolean relayIpHasError = relayIp == null;
/* Parse timestamp parameter. */ - String timestampParameter = request.getParameter("timestamp"); - String timestampStr = parseTimestampParameter(timestampParameter); - final boolean timestampHasError = timestampStr == null; - final boolean timestampTooRecent = !timestampHasError - && checkTimestampTooRecent(timestampStr); + ExoneraTorDate requestedDate + = new ExoneraTorDate(request.getParameter("timestamp"));
/* Parse lang parameter. */ String langParameter = request.getParameter("lang"); @@ -94,20 +84,21 @@ public class ExoneraTorServlet extends HttpServlet { /* Step 2: Query the backend server. */
boolean successfullyConnectedToBackend = false; - String firstDate = null; - String lastDate = null; + ExoneraTorDate firstDate = ExoneraTorDate.INVALID; + ExoneraTorDate lastDate = ExoneraTorDate.INVALID; boolean noRelevantConsensuses = true; List<String[]> statusEntries = new ArrayList<>(); List<String> addressesInSameNetwork = null;
/* Only query, if we received valid user input. */ - if (null != relayIp && !relayIp.isEmpty() && null != timestampStr - && !timestampStr.isEmpty() && !timestampTooRecent) { - QueryResponse queryResponse = this.queryBackend(relayIp, timestampStr); + if (null != relayIp && !relayIp.isEmpty() + && requestedDate.valid && !requestedDate.tooRecent) { + QueryResponse queryResponse + = this.queryBackend(relayIp, requestedDate.asString); if (null != queryResponse) { successfullyConnectedToBackend = true; - firstDate = queryResponse.firstDateInDatabase; - lastDate = queryResponse.lastDateInDatabase; + firstDate = new ExoneraTorDate(queryResponse.firstDateInDatabase); + lastDate = new ExoneraTorDate(queryResponse.lastDateInDatabase); if (null != queryResponse.relevantStatuses && queryResponse.relevantStatuses) { noRelevantConsensuses = false; @@ -148,19 +139,19 @@ public class ExoneraTorServlet extends HttpServlet { this.writeHeader(out, rb, langStr);
/* Write form. */ - boolean timestampOutOfRange = null != timestampStr - && (null != firstDate && timestampStr.compareTo(firstDate) < 0 - || (null != lastDate && timestampStr.compareTo(lastDate) > 0)); + boolean timestampOutOfRange = requestedDate.valid + && (firstDate.valid && requestedDate.date.isBefore(firstDate.date) + || (lastDate.valid && requestedDate.date.isAfter(lastDate.date))); this.writeForm(out, rb, relayIp, relayIpHasError - || ("".equals(relayIp) && !"".equals(timestampStr)), timestampStr, - !relayIpHasError - && !("".equals(relayIp) && !"".equals(timestampStr)) - && (timestampHasError || timestampOutOfRange - || (!"".equals(relayIp) && "".equals(timestampStr))), langStr); + || ("".equals(relayIp) && !requestedDate.empty), + requestedDate.asString, !relayIpHasError + && !("".equals(relayIp) && !requestedDate.valid) + && (!requestedDate.valid || timestampOutOfRange + || (!"".equals(relayIp) && requestedDate.empty)), langStr);
/* If both parameters are empty, don't print any summary and exit. * This is the start page. */ - if ("".equals(relayIp) && "".equals(timestampStr)) { + if ("".equals(relayIp) && requestedDate.empty) { this.writeFooter(out, rb, null, null);
/* If only one parameter is empty and the other is not, print summary @@ -168,7 +159,7 @@ public class ExoneraTorServlet extends HttpServlet { } else if ("".equals(relayIp)) { this.writeSummaryNoIp(out, rb); this.writeFooter(out, rb, null, null); - } else if ("".equals(timestampStr)) { + } else if (requestedDate.empty) { this.writeSummaryNoTimestamp(out, rb); this.writeFooter(out, rb, null, null);
@@ -177,14 +168,14 @@ public class ExoneraTorServlet extends HttpServlet { } else if (relayIpHasError) { this.writeSummaryInvalidIp(out, rb, ipParameter); this.writeFooter(out, rb, null, null); - } else if (timestampHasError) { - this.writeSummaryInvalidTimestamp(out, rb, timestampParameter); + } else if (!requestedDate.valid) { + this.writeSummaryInvalidTimestamp(out, rb, requestedDate.asRequested); this.writeFooter(out, rb, null, null);
/* If the timestamp is too recent, print summary with error message and * exit. */ - } else if (timestampTooRecent) { - this.writeSummaryTimestampTooRecent(out, rb, timestampStr); + } else if (requestedDate.tooRecent) { + this.writeSummaryTimestampTooRecent(out, rb, requestedDate.asString); this.writeFooter(out, rb, null, null);
/* If we were unable to connect to the database, @@ -195,35 +186,36 @@ public class ExoneraTorServlet extends HttpServlet {
/* Similarly, if we found the database to be empty, * write an error message, too. */ - } else if (null == firstDate || null == lastDate) { + } else if (firstDate.empty || lastDate.empty) { this.writeSummaryNoData(out, rb); this.writeFooter(out, rb, null, null);
/* If the requested date is out of range, tell the user. */ } else if (timestampOutOfRange) { - this.writeSummaryTimestampOutsideRange(out, rb, timestampStr, - firstDate, lastDate); - this.writeFooter(out, rb, relayIp, timestampStr); + this.writeSummaryTimestampOutsideRange(out, rb, requestedDate.asString, + firstDate.asString, lastDate.asString); + this.writeFooter(out, rb, relayIp, requestedDate.asString);
} else if (noRelevantConsensuses) { this.writeSummaryNoDataForThisInterval(out, rb); - this.writeFooter(out, rb, relayIp, timestampStr); + this.writeFooter(out, rb, relayIp, requestedDate.asString);
/* Print out result. */ } else { if (!statusEntries.isEmpty()) { - this.writeSummaryPositive(out, rb, relayIp, timestampStr); - this.writeTechnicalDetails(out, rb, relayIp, timestampStr, + this.writeSummaryPositive(out, rb, relayIp, requestedDate.asString); + this.writeTechnicalDetails(out, rb, relayIp, requestedDate.asString, statusEntries); } else if (addressesInSameNetwork != null && !addressesInSameNetwork.isEmpty()) { this.writeSummaryAddressesInSameNetwork(out, rb, relayIp, - timestampStr, langStr, addressesInSameNetwork); + requestedDate.asString, langStr, addressesInSameNetwork); } else { - this.writeSummaryNegative(out, rb, relayIp, timestampStr); + this.writeSummaryNegative(out, rb, relayIp, requestedDate.asString); } - this.writePermanentLink(out, rb, relayIp, timestampStr, langStr); - this.writeFooter(out, rb, relayIp, timestampStr); + this.writePermanentLink(out, rb, relayIp, requestedDate.asString, + langStr); + this.writeFooter(out, rb, relayIp, requestedDate.asString); }
/* Forward to the JSP that adds header and footer. */ @@ -298,35 +290,6 @@ public class ExoneraTorServlet extends HttpServlet { return relayIp; }
- /** Parse a timestamp parameter and return either a non-<code>null</code> - * value in case the parameter was valid or empty, or <code>null</code> if it - * was non-empty and invalid. */ - static String parseTimestampParameter(String passedTimestampParameter) { - String timestampStr = ""; - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); - dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - dateFormat.setLenient(false); - if (passedTimestampParameter != null - && passedTimestampParameter.length() > 0) { - String timestampParameter = passedTimestampParameter.trim(); - try { - long timestamp = dateFormat.parse(timestampParameter).getTime(); - timestampStr = dateFormat.format(timestamp); - } catch (ParseException e) { - timestampStr = null; - } - } - return timestampStr; - } - - /** Return whether the timestamp parameter is too recent, which is the case if - * it matches the day before the current system date (in UTC) or is even - * younger. */ - static boolean checkTimestampTooRecent(String timestampParameter) { - return LocalDate.parse(timestampParameter, ISO_LOCAL_DATE) - .isAfter(LocalDate.now(ZoneOffset.UTC).minusDays(2)); - } - /* Helper method for fetching a query response via URL. */
private QueryResponse queryBackend(String relayIp, String timestampStr) { @@ -698,4 +661,3 @@ public class ExoneraTorServlet extends HttpServlet { out.close(); } } - diff --git a/src/test/java/org/torproject/metrics/exonerator/ExoneraTorDateTest.java b/src/test/java/org/torproject/metrics/exonerator/ExoneraTorDateTest.java new file mode 100644 index 0000000..79690e7 --- /dev/null +++ b/src/test/java/org/torproject/metrics/exonerator/ExoneraTorDateTest.java @@ -0,0 +1,80 @@ +/* Copyright 2017 The Tor Project + * See LICENSE for licensing information */ + +package org.torproject.metrics.exonerator; + +import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE; +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import java.time.LocalDate; +import java.util.Arrays; +import java.util.Collection; + +@RunWith(Parameterized.class) +public class ExoneraTorDateTest { + + /** All test data. */ + @Parameters + public static Collection<Object[]> data() { + return Arrays.asList(new Object[][] { // input, output + {"2000- 10-10", LocalDate.parse("2000-10-10", ISO_LOCAL_DATE), + false, true}, + {"2010-12-16 +0001", LocalDate.parse("2010-12-16", ISO_LOCAL_DATE), + false, true}, + {"2010-12-16 CEST", LocalDate.parse("2010-12-16", ISO_LOCAL_DATE), + false, true}, + {"2010-12-16abcd", LocalDate.parse("2010-12-16", ISO_LOCAL_DATE), + false, true}, + {"2010-12-16", LocalDate.parse("2010-12-16", ISO_LOCAL_DATE), + false, true}, + {"2000-10-10 12:10:00", LocalDate.parse("2000-10-10", ISO_LOCAL_DATE), + false, true}, + {"2000-10-10 1210-04-05", + LocalDate.parse("2000-10-10", ISO_LOCAL_DATE), false, true}, + {"20.10.16", null, false, false}, + {null, null, true, false}, + {"", null, true, false}, + {"2010-12 16", null, false, false}, + {"2010-\t12-\t16", LocalDate.parse("2010-12-16", ISO_LOCAL_DATE), + false, true}, + {"2010- 12- \t16", LocalDate.parse("2010-12-16", ISO_LOCAL_DATE), + false, true}, + {"2003-12-\t16", LocalDate.parse("2003-12-16", ISO_LOCAL_DATE), + false, true}, + {"2004-10-14\t", LocalDate.parse("2004-10-14", ISO_LOCAL_DATE), + false, true}, + {"\n2005-10-12\t\t", LocalDate.parse("2005-10-12", ISO_LOCAL_DATE), + false, true}, + {" 2001-10-13 ", LocalDate.parse("2001-10-13", ISO_LOCAL_DATE), + false, true} + }); + } + + @Parameter(0) + public String dateParameter; + + @Parameter(1) + public LocalDate expectedDate; + + @Parameter(2) + public boolean empty; + + @Parameter(3) + public boolean valid; + + @Test + public void testTimestampParsing() { + ExoneraTorDate date = new ExoneraTorDate(dateParameter); + assertEquals("Input data: " + dateParameter, expectedDate, date.date); + assertEquals("Input data: " + dateParameter, empty, date.empty); + assertEquals("Input data: " + dateParameter, valid, date.valid); + } + +} + diff --git a/src/test/java/org/torproject/metrics/exonerator/ExoneraTorServletTest.java b/src/test/java/org/torproject/metrics/exonerator/ExoneraTorServletTest.java index 16592c3..fc7e8e3 100644 --- a/src/test/java/org/torproject/metrics/exonerator/ExoneraTorServletTest.java +++ b/src/test/java/org/torproject/metrics/exonerator/ExoneraTorServletTest.java @@ -32,34 +32,5 @@ public class ExoneraTorServletTest { assertEquals(data[1], ExoneraTorServlet.parseIpParameter(data[0])); } } - - private static final String[][] timestampTestData - = { // input, output - {"2000- 10-10", "2000-10-10"}, - {"2010-12-16 +0001", "2010-12-16"}, - {"2010-12-16 CEST", "2010-12-16"}, - {"2010-12-16abcd", "2010-12-16"}, - {"2010-12-16", "2010-12-16"}, - {"2000-10-10 12:10:00", "2000-10-10"}, - {"2000-10-10 1210-04-05", "2000-10-10"}, - {"20.10.16", null}, - {null, ""}, - {"", ""}, - {"2010-12 16", null}, - {"2010-\t12-\t16", "2010-12-16"}, - {"2010- 12- \t16", "2010-12-16"}, - {"2003-12-\t16", "2003-12-16"}, - {"2004-10-10\t", "2004-10-10"}, - {"\n2005-10-10\t\t", "2005-10-10"}, - {" 2001-10-10 ", "2001-10-10"} - }; - - @Test - public void testTimestampParsing() { - for (String[] data : timestampTestData) { - assertEquals(data[1], ExoneraTorServlet.parseTimestampParameter(data[0])); - } - } - }