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,