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 4a7631c1d..df6bd0ee8 100644 --- a/Server/src/main/java/org/openas2/app/message/ListMessagesCommand.java +++ b/Server/src/main/java/org/openas2/app/message/ListMessagesCommand.java @@ -8,7 +8,10 @@ import org.openas2.processor.ProcessorModule; import org.openas2.processor.msgtracking.DbTrackingModule; import org.openas2.processor.msgtracking.TrackingModule; +import org.openas2.util.DateUtil; +import java.sql.Timestamp; +import java.text.ParseException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -20,8 +23,10 @@ */ public class ListMessagesCommand extends AliasedMessagesCommand { + private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"; + public String getDefaultDescription() { - return "List all messages"; + return "List messages, optionally bounded by from=/to= (yyyy-MM-dd)"; } @@ -30,22 +35,35 @@ public String getDefaultName() { } public String getDefaultUsage() { - return "list"; + return "list [from=yyyy-MM-dd] [to=yyyy-MM-dd]"; } public CommandResult execute(MessageFactory messageFactory, Object[] params) throws OpenAS2Exception { synchronized (messageFactory) { - + List mpl = getSession().getProcessor().getModulesSupportingAction(TrackingModule.DO_TRACK_MSG); if (mpl == null || mpl.isEmpty()) { CommandResult cmdRes = new CommandResult(CommandResult.TYPE_ERROR); cmdRes.getResults().add("No DB tracking module available."); + return cmdRes; } // Assume we only load one DB tracking module - not sure it makes sense if more than 1 was loaded DbTrackingModule db = (DbTrackingModule) mpl.get(0); - ArrayList> messages = db.listMessages(); + HashMap filters = parseParams(params); + + Timestamp from; + Timestamp to; + 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()); + } + + ArrayList> messages = db.listMessages(from, to); CommandResult cmdRes = new CommandResult(CommandResult.TYPE_OK); @@ -58,4 +76,28 @@ public CommandResult execute(MessageFactory messageFactory, Object[] params) thr return cmdRes; } } + + private HashMap parseParams(Object[] params) { + HashMap filters = new HashMap(); + for (Object param : params) { + String s = param.toString(); + int equalsPos = s.indexOf('='); + if (equalsPos > 0) { + filters.put(s.substring(0, equalsPos), s.substring(equalsPos + 1)); + } + } + return filters; + } + + /** + * @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) + */ + private Timestamp parseBound(String value, String timeOfDay) throws ParseException { + if (value == null) { + return null; + } + return new Timestamp(DateUtil.parseDate(DATE_FORMAT, value + timeOfDay).getTime()); + } } diff --git a/Server/src/main/java/org/openas2/processor/msgtracking/DbTrackingModule.java b/Server/src/main/java/org/openas2/processor/msgtracking/DbTrackingModule.java index d134557db..34735c821 100644 --- a/Server/src/main/java/org/openas2/processor/msgtracking/DbTrackingModule.java +++ b/Server/src/main/java/org/openas2/processor/msgtracking/DbTrackingModule.java @@ -18,6 +18,7 @@ import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.Statement; +import java.sql.Timestamp; import java.sql.Types; import java.util.ArrayList; import java.util.HashMap; @@ -181,27 +182,51 @@ protected synchronized void persist(Message msg, Map map) { } } - public ArrayList> listMessages() { + /** + * List messages, optionally bounded by CREATE_DT. Either bound may be null, in which case + * that side is left open (a null "from" and null "to" together mean no date filter at all). + * + * @param from - lower bound (inclusive) on CREATE_DT, or null for no lower bound + * @param to - upper bound (inclusive) on CREATE_DT, or null for no upper bound + */ + public ArrayList> listMessages(Timestamp from, Timestamp to) { ArrayList> rows = new ArrayList>(); - try (Connection conn = dbHandler.getConnection()) { - Statement s = conn.createStatement(); - ResultSet rs = s.executeQuery("SELECT " + FIELDS.MSG_ID - + ",CREATE_DT,SENDER_ID,RECEIVER_ID,MSG_ID,FILE_NAME,ENCRYPTION_ALGORITHM,SIGNATURE_ALGORITHM,MDN_MODE,STATE FROM " - + tableName); - ResultSetMetaData meta = rs.getMetaData(); - while (rs.next()) { - HashMap row = new HashMap(); - for (int i = 1; i <= meta.getColumnCount(); i++) { - String key = meta.getColumnName(i); - String value = rs.getString(key); - row.put(key, value); + StringBuilder sql = new StringBuilder("SELECT " + FIELDS.MSG_ID + + ",CREATE_DT,SENDER_ID,RECEIVER_ID,MSG_ID,FILE_NAME,ENCRYPTION_ALGORITHM,SIGNATURE_ALGORITHM,MDN_MODE,STATE FROM " + + tableName); + if (from != null) { + sql.append(sql.indexOf(" WHERE ") < 0 ? " WHERE " : " AND ").append("CREATE_DT >= ?"); + } + if (to != null) { + sql.append(sql.indexOf(" WHERE ") < 0 ? " WHERE " : " AND ").append("CREATE_DT <= ?"); + } + sql.append(" ORDER BY CREATE_DT DESC"); + + try (Connection conn = dbHandler.getConnection(); + PreparedStatement s = conn.prepareStatement(sql.toString())) { + int paramIndex = 1; + if (from != null) { + s.setTimestamp(paramIndex++, from); + } + if (to != null) { + s.setTimestamp(paramIndex++, to); + } + try (ResultSet rs = s.executeQuery()) { + ResultSetMetaData meta = rs.getMetaData(); + while (rs.next()) { + HashMap row = new HashMap(); + for (int i = 1; i <= meta.getColumnCount(); i++) { + String key = meta.getColumnName(i); + String value = rs.getString(key); + row.put(key, value); + } + rows.add(row); } - rows.add(row); } } catch (Exception e) { - e.printStackTrace(); + logger.error("Failed to list messages between " + from + " and " + to, e); } return rows; } diff --git a/Server/src/test/java/org/openas2/processor/msgtracking/DbTrackingModuleListMessagesTest.java b/Server/src/test/java/org/openas2/processor/msgtracking/DbTrackingModuleListMessagesTest.java new file mode 100644 index 000000000..ab5bff18f --- /dev/null +++ b/Server/src/test/java/org/openas2/processor/msgtracking/DbTrackingModuleListMessagesTest.java @@ -0,0 +1,119 @@ +package org.openas2.processor.msgtracking; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.openas2.app.BaseServerSetup; +import org.openas2.processor.ProcessorModule; + +import java.nio.file.Files; +import java.nio.file.Paths; +import java.sql.Connection; +import java.sql.Statement; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies that DbTrackingModule.listMessages(from, to) only returns rows whose CREATE_DT + * falls within the requested bound, rather than the whole table (the OOM fix). + */ +@TestInstance(Lifecycle.PER_CLASS) +public class DbTrackingModuleListMessagesTest extends BaseServerSetup { + + private DbTrackingModule db; + + @BeforeAll + public void setUp() throws Exception { + super.createFileSystemResources(this.getClass().getName()); + super.setStartActiveModules(true); + super.setup(); + + List mpl = session.getProcessor().getModulesSupportingAction(TrackingModule.DO_TRACK_MSG); + db = (DbTrackingModule) mpl.get(0); + + try (Connection conn = db.dbHandler.getConnection()) { + Statement s = conn.createStatement(); + // Apply the same schema used in production (Server/src/config/db_ddl.sql) so this + // test exercises listMessages() against the real msg_metadata table shape. + String ddl = new String(Files.readAllBytes(Paths.get("src", "config", "db_ddl.sql"))); + for (String statement : ddl.split(";")) { + if (!statement.trim().isEmpty()) { + s.execute(statement); + } + } + s.executeUpdate(insertSql("msg-before-range", "2020-01-01 10:00:00")); + s.executeUpdate(insertSql("msg-in-range-1", "2026-06-15 09:00:00")); + s.executeUpdate(insertSql("msg-in-range-2", "2026-06-20 23:59:00")); + s.executeUpdate(insertSql("msg-after-range", "2030-01-01 00:00:00")); + } + } + + private String insertSql(String msgId, String createDt) { + return "INSERT INTO msg_metadata (msg_id, sender_id, receiver_id, state, create_dt) VALUES ('" + + msgId + "', 'SENDER', 'RECEIVER', 'MSG_STATUS_MSG_SENT', '" + createDt + "')"; + } + + @AfterAll + public void tearDown() throws Exception { + super.tearDown(); + } + + @Test + public void onlyReturnsRowsWithinTheRequestedDateRange() { + ArrayList> rows = db.listMessages( + Timestamp.valueOf("2026-06-01 00:00:00"), + Timestamp.valueOf("2026-06-30 23:59:59")); + + List ids = new ArrayList(); + for (HashMap row : rows) { + ids.add(row.get("MSG_ID")); + } + + assertEquals(2, rows.size(), "Only the two in-range rows should be returned, not the whole table"); + assertTrue(ids.contains("msg-in-range-1")); + assertTrue(ids.contains("msg-in-range-2")); + assertFalse(ids.contains("msg-before-range")); + assertFalse(ids.contains("msg-after-range")); + } + + @Test + public void returnsNoRowsWhenRangeMatchesNothing() { + ArrayList> rows = db.listMessages( + Timestamp.valueOf("2024-01-01 00:00:00"), + Timestamp.valueOf("2024-01-31 23:59:59")); + + assertTrue(rows.isEmpty()); + } + + @Test + public void returnsEveryRowWhenNoBoundsAreGiven() { + ArrayList> rows = db.listMessages(null, null); + + assertEquals(4, rows.size(), "With no from/to, every row in the table should come back"); + } + + @Test + public void aFromWithNoToIsOpenEnded() { + ArrayList> rows = db.listMessages( + Timestamp.valueOf("2026-01-01 00:00:00"), null); + + List ids = new ArrayList(); + for (HashMap row : rows) { + ids.add(row.get("MSG_ID")); + } + + assertEquals(3, rows.size()); + assertTrue(ids.contains("msg-in-range-1")); + assertTrue(ids.contains("msg-in-range-2")); + assertTrue(ids.contains("msg-after-range")); + assertFalse(ids.contains("msg-before-range")); + } +}