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' + } + +}

