This is an automated email from the ASF dual-hosted git repository.

diveshdut pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git

commit e691f44b3006fd1875bfcff7e79c1099b3ec0e8c
Author: diveshdut <[email protected]>
AuthorDate: Thu Aug 13 15:19:15 2026 +0530

    OFBIZ-13468 Align BOM list APIs with REST query helpers
    
    Adds public sort support and framework list metadata to BOM-related list 
services, including BOM products and BOM type options, while preserving the 
existing service response shapes.
    
    BOM list queries now resolve REST sort expressions into stable EntityQuery 
order-by fields, include generated pagination links for REST calls, and reject 
unsupported sort fields consistently. Focused tests cover sort mappings, 
default ordering, unsupported sort validation, and navigation metadata.
---
 .../manufacturing/servicedef/services_bom.xml      |  16 ++++
 .../manufacturing/api/BomManagementServices.groovy |  44 +++++++--
 .../api/BomManagementServicesTests.groovy          | 105 +++++++++++++++++++++
 3 files changed, 158 insertions(+), 7 deletions(-)

diff --git a/applications/manufacturing/servicedef/services_bom.xml 
b/applications/manufacturing/servicedef/services_bom.xml
index 9be7f1fb72..06552c5993 100644
--- a/applications/manufacturing/servicedef/services_bom.xml
+++ b/applications/manufacturing/servicedef/services_bom.xml
@@ -158,11 +158,15 @@ under the License.
         <attribute mode="IN" name="productAssocTypeId" optional="true" 
type="String"/>
         <attribute mode="IN" name="pageIndex" optional="true" type="Integer"/>
         <attribute mode="IN" name="pageSize" optional="true" type="Integer"/>
+        <attribute mode="IN" name="sort" optional="true" type="String"/>
         <attribute mode="OUT" name="pageIndex" optional="false" 
type="Integer"/>
         <attribute mode="OUT" name="pageSize" optional="false" type="Integer"/>
         <attribute mode="OUT" name="totalCount" optional="false" type="Long"/>
         <attribute mode="OUT" name="totalPages" optional="false" type="Long"/>
         <attribute mode="OUT" name="hasNext" optional="false" type="Boolean"/>
+        <attribute mode="OUT" name="previousPageCount" optional="true" 
type="Long"/>
+        <attribute mode="OUT" name="nextPageCount" optional="true" 
type="Long"/>
+        <attribute mode="OUT" name="links" optional="true" type="Map"/>
         <attribute mode="OUT" name="products" optional="false" 
type="java.util.List"/>
     </service>
     <service name="findProductLookupOptions" engine="groovy" auth="true"
@@ -172,11 +176,15 @@ under the License.
         <attribute mode="IN" name="query" optional="true" type="String"/>
         <attribute mode="IN" name="pageIndex" optional="true" type="Integer"/>
         <attribute mode="IN" name="pageSize" optional="true" type="Integer"/>
+        <attribute mode="IN" name="sort" optional="true" type="String"/>
         <attribute mode="OUT" name="pageIndex" optional="false" 
type="Integer"/>
         <attribute mode="OUT" name="pageSize" optional="false" type="Integer"/>
         <attribute mode="OUT" name="totalCount" optional="false" type="Long"/>
         <attribute mode="OUT" name="totalPages" optional="false" type="Long"/>
         <attribute mode="OUT" name="hasNext" optional="false" type="Boolean"/>
+        <attribute mode="OUT" name="previousPageCount" optional="true" 
type="Long"/>
+        <attribute mode="OUT" name="nextPageCount" optional="true" 
type="Long"/>
+        <attribute mode="OUT" name="links" optional="true" type="Map"/>
         <attribute mode="OUT" name="products" optional="false" 
type="java.util.List"/>
     </service>
     <service name="getBomSnapshot" engine="groovy" auth="true"
@@ -269,11 +277,15 @@ under the License.
         <permission-service service-name="manufacturingPermissionService" 
main-action="VIEW"/>
         <attribute mode="IN" name="pageIndex" optional="true" type="Integer"/>
         <attribute mode="IN" name="pageSize" optional="true" type="Integer"/>
+        <attribute mode="IN" name="sort" optional="true" type="String"/>
         <attribute mode="OUT" name="pageIndex" optional="false" 
type="Integer"/>
         <attribute mode="OUT" name="pageSize" optional="false" type="Integer"/>
         <attribute mode="OUT" name="totalCount" optional="false" type="Long"/>
         <attribute mode="OUT" name="totalPages" optional="false" type="Long"/>
         <attribute mode="OUT" name="hasNext" optional="false" type="Boolean"/>
+        <attribute mode="OUT" name="previousPageCount" optional="true" 
type="Long"/>
+        <attribute mode="OUT" name="nextPageCount" optional="true" 
type="Long"/>
+        <attribute mode="OUT" name="links" optional="true" type="Map"/>
         <attribute mode="OUT" name="facilities" optional="false" 
type="java.util.List"/>
     </service>
     <service name="findBomTypes" engine="groovy" auth="true"
@@ -282,11 +294,15 @@ under the License.
         <permission-service service-name="manufacturingPermissionService" 
main-action="VIEW"/>
         <attribute mode="IN" name="pageIndex" optional="true" type="Integer"/>
         <attribute mode="IN" name="pageSize" optional="true" type="Integer"/>
+        <attribute mode="IN" name="sort" optional="true" type="String"/>
         <attribute mode="OUT" name="pageIndex" optional="false" 
type="Integer"/>
         <attribute mode="OUT" name="pageSize" optional="false" type="Integer"/>
         <attribute mode="OUT" name="totalCount" optional="false" type="Long"/>
         <attribute mode="OUT" name="totalPages" optional="false" type="Long"/>
         <attribute mode="OUT" name="hasNext" optional="false" type="Boolean"/>
+        <attribute mode="OUT" name="previousPageCount" optional="true" 
type="Long"/>
+        <attribute mode="OUT" name="nextPageCount" optional="true" 
type="Long"/>
+        <attribute mode="OUT" name="links" optional="true" type="Map"/>
         <attribute mode="OUT" name="bomTypes" optional="false" 
type="java.util.List"/>
     </service>
 </services>
diff --git 
a/applications/manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/BomManagementServices.groovy
 
b/applications/manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/BomManagementServices.groovy
index 7f60a52bfa..71023bf4d7 100644
--- 
a/applications/manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/BomManagementServices.groovy
+++ 
b/applications/manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/BomManagementServices.groovy
@@ -42,11 +42,17 @@ Map findBomProducts() {
     } catch (IllegalArgumentException e) {
         return ServiceUtil.returnError(e.message)
     }
+    List orderBy
+    try {
+        orderBy = bomProductOrderBy(queryOptions.sort)
+    } catch (IllegalArgumentException e) {
+        return ServiceUtil.returnError(e.message)
+    }
     List bomTypeIds = bomAssocTypeIds()
     if (!bomTypeIds) {
-        return success(RestApiUtil.getPagedResult('products', [], 
queryOptions, 0L, null))
+        return success(RestApiUtil.getPagedResult('products', [], 
queryOptions, 0L,
+                RestApiUtil.getRelativeRequestPath(binding)))
     }
-
     List selectedBomTypeIds = 
UtilValidate.isNotEmpty(parameters.productAssocTypeId)
             ? [parameters.productAssocTypeId]
             : bomTypeIds
@@ -61,7 +67,8 @@ Map findBomProducts() {
         Set matchingProductIds = EntityUtil.searchIds(delegator, 'Product', 
'productId',
                 ['productId', 'productName', 'internalName'], 
parameters.query, 500)
         if (!matchingProductIds) {
-            return success(RestApiUtil.getPagedResult('products', [], 
queryOptions, 0L, null))
+            return success(RestApiUtil.getPagedResult('products', [], 
queryOptions, 0L,
+                    RestApiUtil.getRelativeRequestPath(binding)))
         }
         conditions.add(EntityCondition.makeCondition('productId', 
EntityOperator.IN, matchingProductIds as List))
     }
@@ -70,7 +77,7 @@ Map findBomProducts() {
     EntityQuery query = select('productId', 
'productAssocTypeId').from('ProductAssoc')
             .where(whereCondition).filterByDate().distinct()
     long totalCount = query.queryCount()
-    List rows = query.orderBy('productId', 'productAssocTypeId')
+    List rows = query.orderBy(orderBy)
             .queryPagedList(queryOptions.pageIndex, 
queryOptions.pageSize).getData()
     Set productIds = rows*.productId.findAll { it } as Set
     Map productsById = EntityUtil.lookupById(delegator, 'Product', 
'productId', productIds, false)
@@ -92,7 +99,8 @@ Map findBomProducts() {
                 activeFromDate: componentSummary.activeFromDate
         ]
     }
-    return success(RestApiUtil.getPagedResult('products', products, 
queryOptions, totalCount, null))
+    return success(RestApiUtil.getPagedResult('products', products, 
queryOptions, totalCount,
+            RestApiUtil.getRelativeRequestPath(binding)))
 }
 
 Map getBomSnapshot() {
@@ -355,12 +363,18 @@ Map findBomTypes() {
     } catch (IllegalArgumentException e) {
         return ServiceUtil.returnError(e.message)
     }
+    List orderBy
+    try {
+        orderBy = bomTypeOrderBy(queryOptions.sort)
+    } catch (IllegalArgumentException e) {
+        return ServiceUtil.returnError(e.message)
+    }
     EntityQuery query = from('ProductAssocType')
             .select('productAssocTypeId', 'description')
             .where(parentTypeId: 'PRODUCT_COMPONENT')
             .cache(true)
     long totalCount = query.queryCount()
-    List bomTypes = query.orderBy('description', 'productAssocTypeId')
+    List bomTypes = query.orderBy(orderBy)
             .queryPagedList(queryOptions.pageIndex, 
queryOptions.pageSize).getData()
             .collect { GenericValue productAssocType ->
                 String productAssocTypeId = productAssocType.productAssocTypeId
@@ -371,7 +385,23 @@ Map findBomTypes() {
                         label: description
                 ]
             }
-    return success(RestApiUtil.getPagedResult('bomTypes', bomTypes, 
queryOptions, totalCount, null))
+    return success(RestApiUtil.getPagedResult('bomTypes', bomTypes, 
queryOptions, totalCount,
+            RestApiUtil.getRelativeRequestPath(binding)))
+}
+
+List bomProductOrderBy(String sortExpression) {
+    return RestApiUtil.resolveOrderBy(sortExpression, [
+            productId: 'productId',
+            productAssocTypeId: 'productAssocTypeId',
+            bomType: 'productAssocTypeId'
+    ], ['productId', 'productAssocTypeId'])
+}
+
+List bomTypeOrderBy(String sortExpression) {
+    return RestApiUtil.resolveOrderBy(sortExpression, [
+            productAssocTypeId: 'productAssocTypeId',
+            description: 'description'
+    ], ['description', 'productAssocTypeId'])
 }
 
 List bomAssocTypeIds() {
diff --git 
a/applications/manufacturing/src/test/groovy/org/apache/ofbiz/manufacturing/api/BomManagementServicesTests.groovy
 
b/applications/manufacturing/src/test/groovy/org/apache/ofbiz/manufacturing/api/BomManagementServicesTests.groovy
new file mode 100644
index 0000000000..ccf3ddb89e
--- /dev/null
+++ 
b/applications/manufacturing/src/test/groovy/org/apache/ofbiz/manufacturing/api/BomManagementServicesTests.groovy
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.ofbiz.manufacturing.api
+
+import static org.junit.jupiter.api.Assertions.assertThrows
+
+import org.apache.ofbiz.service.ServiceUtil
+import org.apache.ofbiz.ws.rs.util.RestApiUtil
+import org.apache.ofbiz.ws.rs.util.RestQueryOptions
+import org.junit.jupiter.api.Test
+import org.springframework.mock.web.MockHttpServletRequest
+
+class BomManagementServicesTests {
+
+    @Test
+    void testBomProductSortMapsFrameworkFields() {
+        BomManagementServices services = new BomManagementServices()
+
+        assert services.bomProductOrderBy('-bomType') == 
['-productAssocTypeId', 'productId']
+    }
+
+    @Test
+    void testBomTypeSortMapsFrameworkFields() {
+        BomManagementServices services = new BomManagementServices()
+
+        assert services.bomTypeOrderBy('-productAssocTypeId') == 
['-productAssocTypeId', 'description']
+    }
+
+    @Test
+    void testBomDefaultSortsMatchContracts() {
+        BomManagementServices services = new BomManagementServices()
+
+        assert services.bomProductOrderBy(null) == ['productId', 
'productAssocTypeId']
+        assert services.bomTypeOrderBy(null) == ['description', 
'productAssocTypeId']
+    }
+
+    @Test
+    void testBomProductRejectsUnsupportedSortField() {
+        BomManagementServices services = new BomManagementServices()
+
+        IllegalArgumentException exception = 
assertThrows(IllegalArgumentException) {
+            services.bomProductOrderBy('productName')
+        }
+        assert exception.message == 'Unsupported sort field: productName'
+    }
+
+    @Test
+    void testFindBomProductsRejectsUnsupportedSortBeforeEmptyResults() {
+        BomManagementServices services = new BomManagementServices()
+        services.binding.setVariable('parameters', [sort: 'productName'])
+
+        Map result = services.findBomProducts()
+
+        assert ServiceUtil.isError(result)
+        assert ServiceUtil.getErrorMessage(result) == 'Unsupported sort field: 
productName'
+    }
+
+    @Test
+    void testFrameworkRequestPathReturnsNullWithoutRequestBinding() {
+        BomManagementServices services = new BomManagementServices()
+
+        assert RestApiUtil.getRelativeRequestPath(services.binding) == null
+    }
+
+    @Test
+    void testFrameworkNavigationMetadataAndLinksUseRequestPath() {
+        BomManagementServices services = new BomManagementServices()
+        MockHttpServletRequest request = new MockHttpServletRequest()
+        request.setRequestURI('/rest/manufacturing/boms/products')
+        
request.setQueryString('pageIndex=1&pageSize=1&productAssocTypeId=MANUF_COMPONENT')
+        services.binding.setVariable('request', request)
+        RestQueryOptions queryOptions = RestQueryOptions.fromParameters([
+                pageIndex: 1,
+                pageSize: 1,
+                productAssocTypeId: 'MANUF_COMPONENT'
+        ])
+
+        Map result = RestApiUtil.getPagedResult('products', [], queryOptions, 
3L,
+                RestApiUtil.getRelativeRequestPath(services.binding))
+
+        assert result.previousPageCount == 1L
+        assert result.nextPageCount == 1L
+        assert result.links instanceof Map
+        assert result.links.self.href == 
'/rest/manufacturing/boms/products?productAssocTypeId=MANUF_COMPONENT&pageIndex=1&pageSize=1'
+        assert result.links.prev.href == 
'/rest/manufacturing/boms/products?productAssocTypeId=MANUF_COMPONENT&pageIndex=0&pageSize=1'
+        assert result.links.next.href == 
'/rest/manufacturing/boms/products?productAssocTypeId=MANUF_COMPONENT&pageIndex=2&pageSize=1'
+    }
+
+}

Reply via email to