From 0da1e3dfa0dbae5996bf329fe16bc6878641172d Mon Sep 17 00:00:00 2001 From: Soham Bhambare Date: Wed, 8 Jul 2026 11:28:37 -0400 Subject: [PATCH 1/2] Enhance ListMessagesCommand with time filtering Added optional fromTime=/toTime= (HH:mm:ss) query params to /api/messages/list, layered on top of the existing from=/to= date filters, so callers can bound results to an exact date+time window instead of only whole calendar days. --- .../app/message/ListMessagesCommand.java | 83 +++++++++++++++---- 1 file changed, 67 insertions(+), 16 deletions(-) diff --git a/Server/src/main/java/org/openas2/app/message/ListMessagesCommand.java b/Server/src/main/java/org/openas2/app/message/ListMessagesCommand.java index df6bd0ee..f18f90d0 100644 --- a/Server/src/main/java/org/openas2/app/message/ListMessagesCommand.java +++ b/Server/src/main/java/org/openas2/app/message/ListMessagesCommand.java @@ -26,7 +26,7 @@ public class ListMessagesCommand extends AliasedMessagesCommand { private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"; public String getDefaultDescription() { - return "List messages, optionally bounded by from=/to= (yyyy-MM-dd)"; + return "List messages, optionally bounded by from=/to= (yyyy-MM-dd) and fromTime=/toTime= (HH:mm:ss)"; } @@ -35,7 +35,7 @@ public String getDefaultName() { } public String getDefaultUsage() { - return "list [from=yyyy-MM-dd] [to=yyyy-MM-dd]"; + return "list [from=yyyy-MM-dd] [to=yyyy-MM-dd] [fromTime=HH:mm:ss] [toTime=HH:mm:ss]"; } public CommandResult execute(MessageFactory messageFactory, Object[] params) throws OpenAS2Exception { @@ -53,17 +53,14 @@ public CommandResult execute(MessageFactory messageFactory, Object[] params) thr HashMap filters = parseParams(params); - Timestamp from; - Timestamp to; + DateRange range; try { - from = parseBound(filters.get("from"), " 00:00:00"); - to = parseBound(filters.get("to"), " 23:59:59"); - } catch (ParseException e) { - return new CommandResult(CommandResult.TYPE_ERROR, - "Invalid date - 'from'/'to' must be in yyyy-MM-dd format: " + e.getMessage()); + range = resolveDateRange(filters); + } catch (InvalidRangeException | ParseException e) { + return new CommandResult(CommandResult.TYPE_ERROR, e.getMessage()); } - ArrayList> messages = db.listMessages(from, to); + ArrayList> messages = db.listMessages(range.from, range.to); CommandResult cmdRes = new CommandResult(CommandResult.TYPE_OK); @@ -90,14 +87,68 @@ private HashMap parseParams(Object[] params) { } /** - * @param value - the raw "yyyy-MM-dd" parameter value, or null if the caller didn't supply one - * @param timeOfDay - " 00:00:00" or " 23:59:59" to make the bound inclusive of the whole day - * @return the parsed bound, or null if value was null (i.e. unbounded on that side) + * Resolves the from=/to=/fromTime=/toTime= filters into a concrete date range, applying: + * - fromTime requires from, toTime requires to (a time with no matching date is invalid) + * - to/toTime together require from as well + * - from+fromTime with no to at all defaults the upper bound to right now + * - a resolved from after a resolved to is rejected */ - private Timestamp parseBound(String value, String timeOfDay) throws ParseException { - if (value == null) { + DateRange resolveDateRange(HashMap filters) throws InvalidRangeException, ParseException { + String fromStr = filters.get("from"); + String toStr = filters.get("to"); + String fromTimeStr = filters.get("fromTime"); + String toTimeStr = filters.get("toTime"); + + if (fromTimeStr != null && fromStr == null) { + throw new InvalidRangeException("'fromTime' parameter requires 'from' to also be specified"); + } + if (toTimeStr != null && toStr == null) { + throw new InvalidRangeException("'toTime' parameter requires 'to' to also be specified"); + } + + Timestamp from = parseBound(fromStr, fromTimeStr, "00:00:00"); + Timestamp to; + if (toStr == null && fromStr != null && fromTimeStr != null) { + // from+fromTime given with no 'to' at all: treat as "from that moment until now" + to = new Timestamp(System.currentTimeMillis()); + } else { + to = parseBound(toStr, toTimeStr, "23:59:59"); + } + + if (from != null && to != null && from.after(to)) { + throw new InvalidRangeException("start time cannot be after the end time"); + } + + return new DateRange(from, to); + } + + /** + * @param dateValue - the raw "yyyy-MM-dd" parameter value, or null if the caller didn't supply one + * @param timeValue - the raw "HH:mm:ss" parameter value, or null if the caller didn't supply one + * @param defaultTimeOfDay - "00:00:00" or "23:59:59", used when timeValue is null + * @return the parsed bound, or null if dateValue was null (i.e. unbounded on that side) + */ + private Timestamp parseBound(String dateValue, String timeValue, String defaultTimeOfDay) throws ParseException { + if (dateValue == null) { return null; } - return new Timestamp(DateUtil.parseDate(DATE_FORMAT, value + timeOfDay).getTime()); + String timeOfDay = timeValue != null ? timeValue : defaultTimeOfDay; + return new Timestamp(DateUtil.parseDate(DATE_FORMAT, dateValue + " " + timeOfDay).getTime()); + } + + static final class DateRange { + final Timestamp from; + final Timestamp to; + + DateRange(Timestamp from, Timestamp to) { + this.from = from; + this.to = to; + } + } + + static final class InvalidRangeException extends Exception { + InvalidRangeException(String message) { + super(message); + } } } From b81800b9d069ad0ab36820ba28e0628e0e37d48e Mon Sep 17 00:00:00 2001 From: Soham Bhambare Date: Wed, 8 Jul 2026 11:33:00 -0400 Subject: [PATCH 2/2] Add unit tests for ListMessagesCommand date range MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test file that verifies the from/to/fromTime/toTime resolution logic in ListMessagesCommand — covering valid combinations, defaulting rules, and all the error cases (missing date for a given time, inverted ranges, malformed input). --- .../ListMessagesCommandDateRangeTest.java | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 Server/src/test/java/org/openas2/app/message/ListMessagesCommandDateRangeTest.java diff --git a/Server/src/test/java/org/openas2/app/message/ListMessagesCommandDateRangeTest.java b/Server/src/test/java/org/openas2/app/message/ListMessagesCommandDateRangeTest.java new file mode 100644 index 00000000..6b5b505b --- /dev/null +++ b/Server/src/test/java/org/openas2/app/message/ListMessagesCommandDateRangeTest.java @@ -0,0 +1,157 @@ +package org.openas2.app.message; + +import org.junit.jupiter.api.Test; +import org.openas2.app.message.ListMessagesCommand.DateRange; +import org.openas2.app.message.ListMessagesCommand.InvalidRangeException; + +import java.sql.Timestamp; +import java.text.ParseException; +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Covers the from/to/fromTime/toTime resolution matrix in ListMessagesCommand, independent of + * any DB/session so it can run as a fast, pure-logic unit test. + */ +public class ListMessagesCommandDateRangeTest { + + private final ListMessagesCommand command = new ListMessagesCommand(); + + private HashMap filters(String... kvPairs) { + HashMap map = new HashMap(); + for (int i = 0; i < kvPairs.length; i += 2) { + map.put(kvPairs[i], kvPairs[i + 1]); + } + return map; + } + + @Test + public void noParamsIsFullyUnbounded() throws Exception { + DateRange range = command.resolveDateRange(filters()); + assertNull(range.from); + assertNull(range.to); + } + + @Test + public void fromOnlyIsOpenEnded() throws Exception { + DateRange range = command.resolveDateRange(filters("from", "2026-06-11")); + assertEquals(Timestamp.valueOf("2026-06-11 00:00:00"), range.from); + assertNull(range.to); + } + + @Test + public void toOnlyIsOpenStarted() throws Exception { + DateRange range = command.resolveDateRange(filters("to", "2026-06-11")); + assertNull(range.from); + assertEquals(Timestamp.valueOf("2026-06-11 23:59:59"), range.to); + } + + @Test + public void fromAndToWithNoTimesUseWholeDayDefaults() throws Exception { + DateRange range = command.resolveDateRange(filters("from", "2026-06-11", "to", "2026-06-12")); + assertEquals(Timestamp.valueOf("2026-06-11 00:00:00"), range.from); + assertEquals(Timestamp.valueOf("2026-06-12 23:59:59"), range.to); + } + + @Test + public void fromTimeOnlyDefaultsToEndOfToDay() throws Exception { + DateRange range = command.resolveDateRange( + filters("from", "2026-06-11", "to", "2026-06-12", "fromTime", "08:30:00")); + assertEquals(Timestamp.valueOf("2026-06-11 08:30:00"), range.from); + assertEquals(Timestamp.valueOf("2026-06-12 23:59:59"), range.to); + } + + @Test + public void toTimeOnlyDefaultsToStartOfFromDay() throws Exception { + DateRange range = command.resolveDateRange( + filters("from", "2026-06-11", "to", "2026-06-12", "toTime", "17:45:00")); + assertEquals(Timestamp.valueOf("2026-06-11 00:00:00"), range.from); + assertEquals(Timestamp.valueOf("2026-06-12 17:45:00"), range.to); + } + + @Test + public void bothTimesAreApplied() throws Exception { + DateRange range = command.resolveDateRange( + filters("from", "2026-06-11", "to", "2026-06-12", "fromTime", "08:30:00", "toTime", "17:45:00")); + assertEquals(Timestamp.valueOf("2026-06-11 08:30:00"), range.from); + assertEquals(Timestamp.valueOf("2026-06-12 17:45:00"), range.to); + } + + @Test + public void fromAndFromTimeWithNoToDefaultsToNow() throws Exception { + long before = System.currentTimeMillis(); + DateRange range = command.resolveDateRange(filters("from", "2026-06-11", "fromTime", "08:30:00")); + long after = System.currentTimeMillis(); + + assertEquals(Timestamp.valueOf("2026-06-11 08:30:00"), range.from); + assertNotNull(range.to); + assertTrue(range.to.getTime() >= before && range.to.getTime() <= after, + "Expected 'to' to be resolved to roughly now"); + } + + @Test + public void fromFromTimeAndToTimeWithNoToIsAnError() { + InvalidRangeException e = assertThrows(InvalidRangeException.class, () -> + command.resolveDateRange(filters("from", "2026-06-11", "fromTime", "08:30:00", "toTime", "17:45:00"))); + assertTrue(e.getMessage().contains("toTime")); + } + + @Test + public void fromTimeWithNoFromIsAnError() { + InvalidRangeException e = assertThrows(InvalidRangeException.class, () -> + command.resolveDateRange(filters("to", "2026-06-11", "fromTime", "08:30:00"))); + assertTrue(e.getMessage().contains("fromTime")); + } + + @Test + public void toAndToTimeWithNoFromIsOpenStarted() throws Exception { + DateRange range = command.resolveDateRange(filters("to", "2026-06-11", "toTime", "17:45:00")); + assertNull(range.from); + assertEquals(Timestamp.valueOf("2026-06-11 17:45:00"), range.to); + } + + @Test + public void fromTimeAloneWithNoDatesIsAnError() { + assertThrows(InvalidRangeException.class, () -> + command.resolveDateRange(filters("fromTime", "08:30:00"))); + } + + @Test + public void toTimeAloneWithNoDatesIsAnError() { + assertThrows(InvalidRangeException.class, () -> + command.resolveDateRange(filters("toTime", "17:45:00"))); + } + + @Test + public void malformedDateThrowsParseException() { + assertThrows(ParseException.class, () -> + command.resolveDateRange(filters("from", "not-a-date"))); + } + + @Test + public void malformedTimeThrowsParseException() { + assertThrows(ParseException.class, () -> + command.resolveDateRange(filters("from", "2026-06-11", "fromTime", "not-a-time"))); + } + + @Test + public void invertedTimesOnSameDayIsAnError() { + InvalidRangeException e = assertThrows(InvalidRangeException.class, () -> + command.resolveDateRange(filters( + "from", "2026-06-11", "to", "2026-06-11", + "fromTime", "18:00:00", "toTime", "08:00:00"))); + assertTrue(e.getMessage().contains("start time cannot be after the end time")); + } + + @Test + public void invertedWholeDateRangeIsAnError() { + InvalidRangeException e = assertThrows(InvalidRangeException.class, () -> + command.resolveDateRange(filters("from", "2026-07-01", "to", "2026-06-01"))); + assertTrue(e.getMessage().contains("start time cannot be after the end time")); + } +}