11package io .a2a .extras .pushnotificationconfigstore .database .jpa ;
22
3- import java . util . ArrayList ;
4- import java .util . Collections ;
3+ import jakarta . persistence . TypedQuery ;
4+ import java .time . Instant ;
55import java .util .List ;
66
77import jakarta .annotation .Priority ;
1717import io .a2a .spec .ListTaskPushNotificationConfigResult ;
1818import io .a2a .spec .PushNotificationConfig ;
1919import io .a2a .spec .TaskPushNotificationConfig ;
20+ import java .util .stream .Collectors ;
2021import org .slf4j .Logger ;
2122import org .slf4j .LoggerFactory ;
2223
2627public class JpaDatabasePushNotificationConfigStore implements PushNotificationConfigStore {
2728
2829 private static final Logger LOGGER = LoggerFactory .getLogger (JpaDatabasePushNotificationConfigStore .class );
29-
30+
31+ private static final Instant NULL_TIMESTAMP_SENTINEL = Instant .EPOCH ;
32+
3033 @ PersistenceContext (unitName = "a2a-java" )
3134 EntityManager em ;
3235
@@ -36,6 +39,8 @@ public PushNotificationConfig setInfo(String taskId, PushNotificationConfig noti
3639 // Ensure config has an ID - default to taskId if not provided (mirroring InMemoryPushNotificationConfigStore behavior)
3740 PushNotificationConfig .Builder builder = PushNotificationConfig .builder (notificationConfig );
3841 if (notificationConfig .id () == null || notificationConfig .id ().isEmpty ()) {
42+ // This means the taskId and configId are same. This will not allow having multiple configs for a single Task.
43+ // The configId is a required field in the spec and should not be empty
3944 builder .id (taskId );
4045 }
4146 notificationConfig = builder .build ();
@@ -72,15 +77,61 @@ public PushNotificationConfig setInfo(String taskId, PushNotificationConfig noti
7277 @ Override
7378 public ListTaskPushNotificationConfigResult getInfo (ListTaskPushNotificationConfigParams params ) {
7479 String taskId = params .id ();
75- LOGGER .debug ("Retrieving PushNotificationConfigs for Task '{}'" , taskId );
80+ LOGGER .debug ("Retrieving PushNotificationConfigs for Task '{}' with params: pageSize={}, pageToken={}" ,
81+ taskId , params .pageSize (), params .pageToken ());
7682 try {
77- List <JpaPushNotificationConfig > jpaConfigs = em .createQuery (
78- "SELECT c FROM JpaPushNotificationConfig c WHERE c.id.taskId = :taskId" ,
79- JpaPushNotificationConfig .class )
80- .setParameter ("taskId" , taskId )
81- .getResultList ();
83+ StringBuilder queryBuilder = new StringBuilder ("SELECT c FROM JpaPushNotificationConfig c WHERE c.id.taskId = :taskId" );
84+
85+ if (params .pageToken () != null && !params .pageToken ().isEmpty ()) {
86+ String [] tokenParts = params .pageToken ().split (":" , 2 );
87+ if (tokenParts .length == 2 ) {
88+ // Keyset pagination: get tasks where timestamp < tokenTimestamp OR (timestamp = tokenTimestamp AND id > tokenId)
89+ // All tasks have timestamps (TaskStatus canonical constructor ensures this)
90+ queryBuilder .append (" AND (COALESCE(c.createdAt, :nullSentinel) < :tokenTimestamp OR (COALESCE(c.createdAt, :nullSentinel) = :tokenTimestamp AND c.id.configId > :tokenId))" );
91+ } else {
92+ // Based on the comments in the test case, if the pageToken is invalid start from the beginning.
93+ }
94+ }
95+
96+ queryBuilder .append (" ORDER BY COALESCE(c.createdAt, :nullSentinel) DESC, c.id.configId ASC" );
97+
98+ TypedQuery <JpaPushNotificationConfig > query = em .createQuery (queryBuilder .toString (), JpaPushNotificationConfig .class );
99+ query .setParameter ("taskId" , taskId );
100+ query .setParameter ("nullSentinel" , NULL_TIMESTAMP_SENTINEL );
101+
102+ if (params .pageToken () != null && !params .pageToken ().isEmpty ()) {
103+ String [] tokenParts = params .pageToken ().split (":" , 2 );
104+ if (tokenParts .length == 2 ) {
105+ try {
106+ long timestampMillis = Long .parseLong (tokenParts [0 ]);
107+ String tokenId = tokenParts [1 ];
108+
109+ Instant tokenTimestamp = Instant .ofEpochMilli (timestampMillis );
110+ query .setParameter ("tokenTimestamp" , tokenTimestamp );
111+ query .setParameter ("tokenId" , tokenId );
112+ } catch (NumberFormatException e ) {
113+ // Malformed timestamp in pageToken
114+ throw new io .a2a .spec .InvalidParamsError (null ,
115+ "Invalid pageToken format: timestamp must be numeric milliseconds" , null );
116+ }
117+ }
118+ }
119+
120+ int pageSize = params .getEffectivePageSize ();
121+ query .setMaxResults (pageSize + 1 );
122+ List <JpaPushNotificationConfig > jpaConfigsPage = query .getResultList ();
123+
124+ String nextPageToken = null ;
125+ if (jpaConfigsPage .size () > pageSize ) {
126+ // There are more results than the page size, and in this case, a nextToken should be created with the last item.
127+ // Format: "timestamp_millis:taskId" for keyset pagination
128+ jpaConfigsPage = jpaConfigsPage .subList (0 , pageSize );
129+ JpaPushNotificationConfig lastConfig = jpaConfigsPage .get (jpaConfigsPage .size () - 1 );
130+ Instant timestamp = lastConfig .getCreatedAt () != null ? lastConfig .getCreatedAt () : NULL_TIMESTAMP_SENTINEL ;
131+ nextPageToken = timestamp .toEpochMilli () + ":" + lastConfig .getId ().getConfigId ();
132+ }
82133
83- List <PushNotificationConfig > configs = jpaConfigs .stream ()
134+ List <PushNotificationConfig > configs = jpaConfigsPage .stream ()
84135 .map (jpaConfig -> {
85136 try {
86137 return jpaConfig .getConfig ();
@@ -95,57 +146,17 @@ public ListTaskPushNotificationConfigResult getInfo(ListTaskPushNotificationConf
95146
96147 LOGGER .debug ("Successfully retrieved {} PushNotificationConfigs for Task '{}'" , configs .size (), taskId );
97148
98- // Handle pagination
99- if (configs .isEmpty ()) {
100- return new ListTaskPushNotificationConfigResult (Collections .emptyList ());
101- }
102-
103- if (params .pageSize () <= 0 ) {
104- return new ListTaskPushNotificationConfigResult (convertPushNotificationConfig (configs , params ), null );
105- }
106-
107- // Apply pageToken filtering if provided
108- List <PushNotificationConfig > paginatedConfigs = configs ;
109- if (params .pageToken () != null && !params .pageToken ().isBlank ()) {
110- int index = findFirstIndex (configs , params .pageToken ());
111- if (index < configs .size ()) {
112- paginatedConfigs = configs .subList (index , configs .size ());
113- }
114- }
115-
116- // Apply page size limit
117- if (paginatedConfigs .size () <= params .pageSize ()) {
118- return new ListTaskPushNotificationConfigResult (convertPushNotificationConfig (paginatedConfigs , params ), null );
119- }
149+ List <TaskPushNotificationConfig > taskPushNotificationConfigs = configs .stream ()
150+ .map (config -> new TaskPushNotificationConfig (params .id (), config , params .tenant ()))
151+ .collect (Collectors .toList ());
120152
121- String nextToken = paginatedConfigs .get (params .pageSize ()).token ();
122- return new ListTaskPushNotificationConfigResult (
123- convertPushNotificationConfig (paginatedConfigs .subList (0 , params .pageSize ()), params ),
124- nextToken );
153+ return new ListTaskPushNotificationConfigResult (taskPushNotificationConfigs , nextPageToken );
125154 } catch (Exception e ) {
126155 LOGGER .error ("Failed to retrieve PushNotificationConfigs for Task '{}'" , taskId , e );
127156 throw e ;
128157 }
129158 }
130159
131- private int findFirstIndex (List <PushNotificationConfig > configs , String token ) {
132- for (int i = 0 ; i < configs .size (); i ++) {
133- if (token .equals (configs .get (i ).token ())) {
134- return i ;
135- }
136- }
137- return configs .size ();
138- }
139-
140- private List <TaskPushNotificationConfig > convertPushNotificationConfig (List <PushNotificationConfig > pushNotificationConfigList , ListTaskPushNotificationConfigParams params ) {
141- List <TaskPushNotificationConfig > taskPushNotificationConfigList = new ArrayList <>(pushNotificationConfigList .size ());
142- for (PushNotificationConfig pushNotificationConfig : pushNotificationConfigList ) {
143- TaskPushNotificationConfig taskPushNotificationConfig = new TaskPushNotificationConfig (params .id (), pushNotificationConfig , params .tenant ());
144- taskPushNotificationConfigList .add (taskPushNotificationConfig );
145- }
146- return taskPushNotificationConfigList ;
147- }
148-
149160 @ Transactional
150161 @ Override
151162 public void deleteInfo (String taskId , String configId ) {
0 commit comments