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

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


The following commit(s) were added to refs/heads/trunk by this push:
     new a8dcb5bac Added support to define nested rest api resource (#144)
a8dcb5bac is described below

commit a8dcb5bac42ebeddc02c7ba39e91038c3028d505
Author: Deepak Dixit <[email protected]>
AuthorDate: Thu Oct 23 17:54:14 2025 +0530

    Added support to define nested rest api resource (#144)
---
 rest-api/dtd/rest-api.xsd                          |   1 +
 .../apache/ofbiz/ws/rs/core/OFBizApiConfig.java    |  99 +++++++-----
 .../apache/ofbiz/ws/rs/model/ModelApiReader.java   |  32 +++-
 .../apache/ofbiz/ws/rs/model/ModelResource.java    |   8 +
 .../ofbiz/ws/rs/openapi/OFBizOpenApiReader.java    | 169 ++++++++++++---------
 5 files changed, 192 insertions(+), 117 deletions(-)

diff --git a/rest-api/dtd/rest-api.xsd b/rest-api/dtd/rest-api.xsd
index 1c74d6481..43273abb3 100644
--- a/rest-api/dtd/rest-api.xsd
+++ b/rest-api/dtd/rest-api.xsd
@@ -34,6 +34,7 @@ under the License.
     <xs:element name="resource">
         <xs:complexType>
             <xs:sequence>
+                <xs:element minOccurs="0" maxOccurs="unbounded" 
ref="resource"/> <!-- allow nesting -->
                 <xs:element minOccurs="0" maxOccurs="unbounded" 
ref="operation"/>
             </xs:sequence>
             <xs:attribute name="name" type="xs:string" use="required"/>
diff --git 
a/rest-api/src/main/java/org/apache/ofbiz/ws/rs/core/OFBizApiConfig.java 
b/rest-api/src/main/java/org/apache/ofbiz/ws/rs/core/OFBizApiConfig.java
index bec8bf94d..644bb1bd4 100644
--- a/rest-api/src/main/java/org/apache/ofbiz/ws/rs/core/OFBizApiConfig.java
+++ b/rest-api/src/main/java/org/apache/ofbiz/ws/rs/core/OFBizApiConfig.java
@@ -117,46 +117,65 @@ public class OFBizApiConfig extends ResourceConfig {
             Debug.logInfo("No API definitions to process", MODULE);
             return;
         }
-        MICRO_APIS.forEach((k, v) -> {
-            Debug.logInfo("Registring Resource Definitions from API - " + k, 
MODULE);
-            List<ModelResource> resources = v.getResources();
-            String entryPath = v.getPath();
-            resources.forEach(modelResource -> {
-                if (modelResource.isPublish()) {
-                    String path = entryPath + "/" + modelResource.getPath() + 
"/";
-                    Resource.Builder resourceBuilder = Resource.builder(path)
-                            .name(modelResource.getName());
-                    for (ModelOperation op : modelResource.getOperations()) {
-                        String verb = op.getVerb().toUpperCase();
-                        boolean isOtherThanGet = verb.matches(HttpMethod.POST 
+ "|" + HttpMethod.PUT + "|" + HttpMethod.PATCH);
-                        if (UtilValidate.isEmpty(op.getPath())) { // Add the 
method to the parent resource
-                            ResourceMethod.Builder methodBuilder = 
resourceBuilder.addMethod(verb);
-                            methodBuilder.produces(MediaType.APPLICATION_JSON);
-                            if (isOtherThanGet) {
-                                
methodBuilder.consumes(MediaType.APPLICATION_JSON);
-                            }
-                            if (op.isAuth()) {
-                                methodBuilder.nameBindings(Secured.class);
-                            }
-                            String serviceName = op.getService();
-                            methodBuilder.handledBy(new 
ServiceRequestHandler(serviceName));
-                        } else {
-                            Resource.Builder childResourceBuilder = 
resourceBuilder.addChildResource(op.getPath());
-                            ResourceMethod.Builder childResourceMethodBuilder 
= childResourceBuilder.addMethod(verb);
-                            
childResourceMethodBuilder.produces(MediaType.APPLICATION_JSON);
-                            if (isOtherThanGet) {
-                                
childResourceMethodBuilder.consumes(MediaType.APPLICATION_JSON);
-                            }
-                            if (op.isAuth()) {
-                                
childResourceMethodBuilder.nameBindings(Secured.class);
-                            }
-                            String serviceName = op.getService();
-                            childResourceMethodBuilder.handledBy(new 
ServiceRequestHandler(serviceName));
-                        }
-                    }
-                    registerResources(resourceBuilder.build());
-                }
-            });
+
+        MICRO_APIS.forEach((apiPath, modelApi) -> {
+            Debug.logInfo("Registering Resource Definitions from API - " + 
apiPath, MODULE);
+            for (ModelResource resource : modelApi.getResources()) {
+                String resourcePath = buildCleanPath(apiPath, 
resource.getPath());
+                registerModelResource(resource, resourcePath);
+            }
         });
     }
+    private void registerModelResource(ModelResource modelResource, String 
basePath) {
+        if (!modelResource.isPublish()) return;
+
+        Resource.Builder resourceBuilder = Resource.builder("/" + basePath)
+                .name(modelResource.getName());
+
+        for (ModelOperation op : modelResource.getOperations()) {
+            String verb = op.getVerb().toUpperCase();
+            boolean isOtherThanGet = verb.matches(HttpMethod.POST + "|" + 
HttpMethod.PUT + "|" + HttpMethod.PATCH);
+            String opPath = op.getPath();
+
+            ResourceMethod.Builder methodBuilder;
+            if (UtilValidate.isEmpty(opPath)) {
+                methodBuilder = resourceBuilder.addMethod(verb);
+            } else {
+                Resource.Builder childBuilder = 
resourceBuilder.addChildResource(opPath);
+                methodBuilder = childBuilder.addMethod(verb);
+            }
+
+            methodBuilder.produces(MediaType.APPLICATION_JSON);
+            if (isOtherThanGet) {
+                methodBuilder.consumes(MediaType.APPLICATION_JSON);
+            }
+            if (op.isAuth()) {
+                methodBuilder.nameBindings(Secured.class);
+            }
+            methodBuilder.handledBy(new 
ServiceRequestHandler(op.getService()));
+        }
+
+        // Register the current resource
+        registerResources(resourceBuilder.build());
+
+        // Recursively process nested sub-resources
+        if (UtilValidate.isNotEmpty(modelResource.getSubResources())) {
+            for (ModelResource sub : modelResource.getSubResources()) {
+                String subPath = buildCleanPath(basePath, sub.getPath());
+                registerModelResource(sub, subPath);
+            }
+        }
+    }
+    private String buildCleanPath(String... parts) {
+        StringBuilder pathBuilder = new StringBuilder();
+        for (String part : parts) {
+            if (part == null || part.trim().isEmpty()) continue;
+            part = part.replaceAll("^/+", "").replaceAll("/+$", ""); // trim 
slashes
+            if (!part.isEmpty()) {
+                if (pathBuilder.length() > 0) pathBuilder.append('/');
+                pathBuilder.append(part);
+            }
+        }
+        return pathBuilder.toString();
+    }
 }
diff --git 
a/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApiReader.java 
b/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApiReader.java
index e33e4733b..c21d928e3 100644
--- a/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApiReader.java
+++ b/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApiReader.java
@@ -58,16 +58,38 @@ public final class ModelApiReader {
         return api;
     }
 
-    private static void createModelResource(Element resourceEle, ModelApi 
modelApi) {
-        ModelResource resource = new 
ModelResource().name(UtilXml.checkEmpty(resourceEle.getAttribute("name")).intern())
+    private static void createModelResource(Element resourceEle, ModelApi api) 
{
+        ModelResource resource = buildResource(resourceEle);
+        createOperations(resourceEle, resource);
+
+        // recursive children
+        for (Element nestedResourceEle : UtilXml.childElementList(resourceEle, 
"resource")) {
+            createModelResource(nestedResourceEle, resource);
+        }
+
+        Debug.logInfo(resource.toString(), MODULE);
+        api.addResource(resource);
+    }
+
+    private static void createModelResource(Element resourceEle, ModelResource 
parentResource) {
+        ModelResource resource = buildResource(resourceEle);
+        createOperations(resourceEle, resource);
+
+        for (Element nestedResourceEle : UtilXml.childElementList(resourceEle, 
"resource")) {
+            createModelResource(nestedResourceEle, resource);
+        }
+
+        Debug.logInfo(resource.toString(), MODULE);
+        parentResource.addSubResource(resource);
+    }
+    private static ModelResource buildResource(Element resourceEle) {
+        return new ModelResource()
+                
.name(UtilXml.checkEmpty(resourceEle.getAttribute("name")).intern())
                 
.description(UtilXml.checkEmpty(resourceEle.getAttribute("description")).intern())
                 
.displayName(UtilXml.checkEmpty(resourceEle.getAttribute("displayName")).intern())
                 
.path(UtilXml.checkEmpty(resourceEle.getAttribute("path")).intern())
                 
.publish(Boolean.parseBoolean(UtilXml.checkEmpty(resourceEle.getAttribute("publish")).intern()))
                 
.auth(Boolean.parseBoolean(UtilXml.checkEmpty(resourceEle.getAttribute("auth")).intern()));
-        createOperations(resourceEle, resource);
-        Debug.logInfo(resource.toString(), MODULE);
-        modelApi.addResource(resource);
     }
 
     private static void createOperations(Element resourceEle, ModelResource 
resource) {
diff --git 
a/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelResource.java 
b/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelResource.java
index a27a0e3a5..769cfced9 100644
--- a/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelResource.java
+++ b/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelResource.java
@@ -24,6 +24,7 @@ import java.util.List;
 public class ModelResource {
 
     private List<ModelOperation> operations;
+    private List<ModelResource> subResources = new ArrayList<>();
     private String name;
     private String path;
     private String displayName;
@@ -163,6 +164,13 @@ public class ModelResource {
         this.description = description;
     }
 
+    public void addSubResource(ModelResource resource) {
+        this.subResources.add(resource);
+    }
+
+    public List<ModelResource> getSubResources() {
+        return subResources;
+    }
     /**
      * @param description
      * @return
diff --git 
a/rest-api/src/main/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiReader.java 
b/rest-api/src/main/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiReader.java
index 7c2dc76ba..2c93d8ea1 100644
--- 
a/rest-api/src/main/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiReader.java
+++ 
b/rest-api/src/main/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiReader.java
@@ -104,78 +104,101 @@ public final class OFBizOpenApiReader extends Reader 
implements OpenApiReader {
         Map<String, ModelApi> apis = OFBizApiConfig.getModelApis();
         SecurityRequirement security = new SecurityRequirement();
         security.addList("jwtToken");
-        apis.forEach((k, v) -> {
-            if (!v.isPublish()) {
-                return;
+
+        apis.forEach((k, api) -> {
+            if (!api.isPublish()) return;
+
+            List<String> baseSegments = new ArrayList<>();
+            baseSegments.add(api.getPath());
+
+            for (ModelResource resource : api.getResources()) {
+                processResourceRecursive(resource, baseSegments, security);
             }
-            List<ModelResource> resources = v.getResources();
-            resources.forEach(modelResource -> {
-                List<String> segments = new ArrayList<>();
-                segments.add(v.getPath());
-                Tag resourceTab = new 
Tag().name(modelResource.getDisplayName()).description(modelResource.getDescription());
-                openApi.addTagsItem(resourceTab);
-                segments.add(modelResource.getPath());
-                for (ModelOperation op : modelResource.getOperations()) {
-                    segments.add(op.getPath());
-                    String uri = buildNestedUrl(segments);
-                    boolean pathExists = false;
-                    PathItem pathItemObject = paths.get(uri);
-                    if (UtilValidate.isEmpty(pathItemObject)) {
-                        pathItemObject = new PathItem();
-                    } else {
-                        pathExists = true;
-                    }
-                    String serviceName = op.getService();
-                    final Operation operation = new 
Operation().summary(op.getDescription())
-                            
.description(op.getDescription()).addTagsItem(modelResource.getDisplayName())
-                            
.operationId(serviceName).deprecated(false).addSecurityItem(security);
-                    String verb = op.getVerb().toUpperCase();
-                    ModelService service = null;
-                    try {
-                        service = context.getModelService(serviceName);
-                    } catch (GenericServiceException e) {
-                        Debug.logError("Service '" + serviceName + "' not 
found while trying to map REST resource " + uri + "; ignoring. ", MODULE);
-                        continue;
-                    }
-                    if (verb.equalsIgnoreCase(HttpMethod.GET)) {
-                        final QueryParameter serviceInParam = (QueryParameter) 
new QueryParameter().required(true)
-                                .description("Operation Input Parameters in 
JSON").name("input");
-                        Schema<?> refSchema = new Schema<>();
-                        refSchema.$ref("#/components/schemas/" + 
"api.request." + service.getName());
-                        serviceInParam.content(new 
Content().addMediaType(jakarta.ws.rs.core.MediaType.APPLICATION_JSON,
-                                new MediaType().schema(refSchema)));
-                        operation.addParametersItem(serviceInParam);
-                    } else if (verb.matches(HttpMethod.POST + "|" + 
HttpMethod.PUT + "|" + HttpMethod.PATCH)) {
-                        RequestBody request = new RequestBody()
-                                .description("Request Body for operation " + 
op.getDescription())
-                                .content(new 
Content().addMediaType(jakarta.ws.rs.core.MediaType.APPLICATION_JSON,
-                                        new MediaType().schema(new Schema<>()
-                                                .$ref("#/components/schemas/" 
+ "api.request." + service.getName()))));
-                        operation.setRequestBody(request);
-                        operation.addParametersItem(HEADER_CONTENT_TYPE_JSON);
-                    }
-                    List<String> pathParams = 
RestApiUtil.getPathParameters(uri);
-                    for (String pathParam : pathParams) {
-                        ModelParam mdParam = 
service.getInModelParamList().stream()
-                                .filter(param -> (!param.getInternal() && 
pathParam.equals(param.getName())))
-                                .findFirst().orElse(null);
-                        final PathParameter pathParameter = (PathParameter) 
new PathParameter().required(true)
-                                .description(mdParam != null ? 
mdParam.getShortDisplayDescription() : "")
-                                .name(pathParam)
-                                
.schema(OpenApiUtil.getAttributeSchema(service, mdParam));
-                        operation.addParametersItem(pathParameter);
-                    }
-                    addServiceOutSchema(service);
-                    addServiceInSchema(service);
-                    addServiceOperationApiResponses(service, operation);
-                    setPathItemOperation(pathItemObject, verb.toUpperCase(), 
operation);
-                    if (!pathExists) {
-                        paths.addPathItem(uri, pathItemObject);
-                    }
-                }
-            });
         });
     }
+    private void processResourceRecursive(ModelResource resource, List<String> 
parentSegments, SecurityRequirement security) {
+        List<String> currentSegments = new ArrayList<>(parentSegments);
+        currentSegments.add(resource.getPath());
+
+        Tag resourceTag = new 
Tag().name(resource.getDisplayName()).description(resource.getDescription());
+        openApi.addTagsItem(resourceTag);
+
+        for (ModelOperation op : resource.getOperations()) {
+            List<String> fullPathSegments = new ArrayList<>(currentSegments);
+            fullPathSegments.add(op.getPath());
+            String uri = buildNestedUrl(fullPathSegments);
+
+            PathItem pathItemObject = paths.get(uri);
+            boolean pathExists = pathItemObject != null;
+            if (!pathExists) {
+                pathItemObject = new PathItem();
+            }
+
+            String serviceName = op.getService();
+            ModelService service;
+            try {
+                service = context.getModelService(serviceName);
+            } catch (GenericServiceException e) {
+                Debug.logError("Service '" + serviceName + "' not found while 
trying to map REST resource " + uri + "; ignoring. ", MODULE);
+                continue;
+            }
+
+            Operation operation = new Operation()
+                    .summary(op.getDescription())
+                    .description(op.getDescription())
+                    .addTagsItem(resource.getDisplayName())
+                    .operationId(serviceName)
+                    .deprecated(false)
+                    .addSecurityItem(security);
+
+            String verb = op.getVerb().toUpperCase();
+            if (verb.equalsIgnoreCase(HttpMethod.GET)) {
+                QueryParameter serviceInParam = (QueryParameter) new 
QueryParameter().required(true)
+                        .description("Operation Input Parameters in 
JSON").name("input");
+
+                Schema<?> refSchema = new 
Schema<>().$ref("#/components/schemas/api.request." + service.getName());
+                serviceInParam.content(new 
Content().addMediaType(jakarta.ws.rs.core.MediaType.APPLICATION_JSON,
+                        new MediaType().schema(refSchema)));
+                operation.addParametersItem(serviceInParam);
+            } else if (verb.matches(HttpMethod.POST + "|" + HttpMethod.PUT + 
"|" + HttpMethod.PATCH)) {
+                RequestBody request = new RequestBody()
+                        .description("Request Body for operation " + 
op.getDescription())
+                        .content(new 
Content().addMediaType(jakarta.ws.rs.core.MediaType.APPLICATION_JSON,
+                                new MediaType().schema(new 
Schema<>().$ref("#/components/schemas/api.request." + service.getName()))));
+                operation.setRequestBody(request);
+                operation.addParametersItem(HEADER_CONTENT_TYPE_JSON);
+            }
+
+            List<String> pathParams = RestApiUtil.getPathParameters(uri);
+            for (String pathParam : pathParams) {
+                ModelParam mdParam = service.getInModelParamList().stream()
+                        .filter(param -> (!param.getInternal() && 
pathParam.equals(param.getName())))
+                        .findFirst().orElse(null);
+                PathParameter pathParameter = new PathParameter();
+                pathParameter.setRequired(true);
+                pathParameter.setName(pathParam);
+                pathParameter.setDescription(mdParam != null ? 
mdParam.getShortDisplayDescription() : "");
+                
pathParameter.setSchema(OpenApiUtil.getAttributeSchema(service, mdParam));
+                operation.addParametersItem(pathParameter);
+            }
+
+            addServiceOutSchema(service);
+            addServiceInSchema(service);
+            addServiceOperationApiResponses(service, operation);
+            setPathItemOperation(pathItemObject, verb.toUpperCase(), 
operation);
+
+            if (!pathExists) {
+                paths.addPathItem(uri, pathItemObject);
+            }
+        }
+
+        //Recursively process nested resources
+        if (resource.getSubResources() != null) {
+            for (ModelResource sub : resource.getSubResources()) {
+                processResourceRecursive(sub, currentSegments, security);
+            }
+        }
+    }
 
     public static String buildNestedUrl(List<String> segments) {
         StringBuilder pathBuilder = new StringBuilder();
@@ -212,9 +235,11 @@ public final class OFBizOpenApiReader extends Reader 
implements OpenApiReader {
                 if (service.getAction().equalsIgnoreCase(HttpMethod.GET)) {
                     boolean inParamsEmpty = 
UtilValidate.isEmpty(service.getInParamNamesMap());
                     if (!inParamsEmpty) {
-                        final QueryParameter serviceInParam = (QueryParameter) 
new QueryParameter()
-                                .required(!inParamsEmpty)
-                                .description("Service In Parameters in 
JSON").name("inParams");
+                        QueryParameter serviceInParam = new QueryParameter();
+                        serviceInParam.setRequired(true);
+                        serviceInParam.setDescription("Operation Input 
Parameters in JSON");
+                        serviceInParam.setName("input");
+
                         Schema<?> refSchema = new Schema<>();
                         refSchema.$ref("#/components/schemas/" + 
"api.request." + service.getName());
                         serviceInParam.content(new 
Content().addMediaType(jakarta.ws.rs.core.MediaType.APPLICATION_JSON,

Reply via email to