Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)";
}


Expand All @@ -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 {
Expand All @@ -53,17 +53,14 @@ public CommandResult execute(MessageFactory messageFactory, Object[] params) thr

HashMap<String, String> 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<HashMap<String,String>> messages = db.listMessages(from, to);
ArrayList<HashMap<String,String>> messages = db.listMessages(range.from, range.to);

CommandResult cmdRes = new CommandResult(CommandResult.TYPE_OK);

Expand All @@ -90,14 +87,68 @@ private HashMap<String, String> 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<String, String> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> filters(String... kvPairs) {
HashMap<String, String> map = new HashMap<String, String>();
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"));
}
}
Loading