veeam-control-service: fix vm pagination (#13383)

Only consider page in pagination for search clause
https://ovirt.github.io/ovirt-engine-api-model/4.5/#_pagination

Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/request/ListQuery.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/request/ListQuery.java
index f57edf7..1cb560a 100644
--- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/request/ListQuery.java
+++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/request/ListQuery.java
@@ -22,6 +22,8 @@
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import javax.servlet.http.HttpServletRequest;
 
@@ -31,6 +33,8 @@
 import org.jetbrains.annotations.NotNull;
 
 public class ListQuery {
+    private static final Pattern PAGE_CLAUSE_PATTERN = Pattern.compile("(?i)\\bpage\\s+(\\d+)\\b");
+
     boolean allContent;
     Long max;
     Long page;
@@ -53,6 +57,14 @@
         this.max = max;
     }
 
+    public Long getPage() {
+        return page;
+    }
+
+    public void setPage(Long page) {
+        this.page = page;
+    }
+
     public void setSearch(Map<String, String> search) {
         this.search = search;
     }
@@ -108,10 +120,13 @@
         query.setFollow(follow);
         Map<String, String> searchItems = getSearchMap(request.getParameter("search"));
         if (!searchItems.isEmpty()) {
-            try {
-                query.setMax(Long.parseLong(searchItems.get("page")));
-            } catch (NumberFormatException e) {
-                // Ignore invalid page and keep default null value.
+            String pageValue = searchItems.get("page");
+            if (StringUtils.isNotBlank(pageValue)) {
+                try {
+                    query.setPage(Long.parseLong(pageValue));
+                } catch (NumberFormatException e) {
+                    // Ignore invalid page and keep default null value.
+                }
             }
             query.setSearch(searchItems);
         }
@@ -119,39 +134,19 @@
         return query;
     }
 
-    // Parse search clause. Only keep items which use simple '=' operator, and ignore others. For example:
-    //   name=myvm and status=up  --> {name=myvm, status=up}
-    //   name=myvm and status!=down --> {name=myvm} (ignore status!=down because it uses '!=' operator)
+    // Parse search clause. For now, only extract the oVirt paging clause.
+    // Examples:
+    //   page 3 --> {page=3}
+    //   sortby name page 2 --> {page=2}
     @NotNull
     private static Map<String, String> getSearchMap(String searchClause) {
         Map<String, String> searchItems = new LinkedHashMap<>();
         if (StringUtils.isBlank(searchClause)) {
             return searchItems;
         }
-        String[] terms = searchClause.trim().split("(?i)\\s+and\\s+");
-        for (String term : terms) {
-            if (term == null) {
-                continue;
-            }
-            String trimmedTerm = term.trim();
-            if (trimmedTerm.isEmpty()) {
-                continue;
-            }
-
-            int eqIdx = trimmedTerm.indexOf('=');
-            if (eqIdx <= 0 || eqIdx != trimmedTerm.lastIndexOf('=')) {
-                continue;
-            }
-            char prev = trimmedTerm.charAt(eqIdx - 1);
-            if (prev == '!' || prev == '<' || prev == '>') {
-                continue;
-            }
-
-            String key = trimmedTerm.substring(0, eqIdx).trim();
-            String value = trimmedTerm.substring(eqIdx + 1).trim();
-            if (!key.isEmpty() && !value.isEmpty()) {
-                searchItems.put(key, value);
-            }
+        Matcher matcher = PAGE_CLAUSE_PATTERN.matcher(searchClause);
+        if (matcher.find()) {
+            searchItems.put("page", matcher.group(1));
         }
         return searchItems;
     }
diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/request/ListQueryTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/request/ListQueryTest.java
index 16fbd02..a6d8bfe 100644
--- a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/request/ListQueryTest.java
+++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/request/ListQueryTest.java
@@ -67,18 +67,18 @@
     }
 
     @Test
-    public void testFromRequest_SearchParserIgnoresNonEqualsAndUsesPageValueAsMaxCurrentBehavior() {
+    public void testFromRequest_SearchParserExtractsPageOnly() {
         final HttpServletRequest request = mock(HttpServletRequest.class);
-        when(request.getParameterMap()).thenReturn(Map.of("search", new String[]{"name=vm and page=3 and status!=down and x>=1"}));
+        when(request.getParameterMap()).thenReturn(Map.of("search", new String[]{"sortby name page 3"}));
         when(request.getParameter("all_content")).thenReturn(null);
         when(request.getParameter("max")).thenReturn(null);
         when(request.getParameter("follow")).thenReturn(null);
-        when(request.getParameter("search")).thenReturn("name=vm and page=3 and status!=down and x>=1");
+        when(request.getParameter("search")).thenReturn("sortby name page 3");
 
         final ListQuery query = ListQuery.fromRequest(request);
 
-        // Document existing behavior: when search contains page=..., max is set from it.
-        org.junit.Assert.assertEquals(Long.valueOf(3L), query.getLimit());
+        // Only page key is extracted from search clause in oVirt format "page N"
+        org.junit.Assert.assertEquals(Long.valueOf(3L), query.getPage());
     }
 
     @Test