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

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


The following commit(s) were added to refs/heads/trunk by this push:
     new 3dece2e2e4 Implemented: return of additional / custom http status 
codes including publishing to Swagger UI (OFBIZ-12516)
3dece2e2e4 is described below

commit 3dece2e2e407d5dea2225b8855f58574e14b734d
Author: Lukas Finster <[email protected]>
AuthorDate: Thu Aug 6 12:44:29 2026 +0200

    Implemented: return of additional / custom http status codes including
    publishing to Swagger UI (OFBIZ-12516)
    
    * Allows the return of custom http status codes. Custom status codes can
    * be defined in ResponseStatus.java with examples for swagger UI
    * inOpenApiUtil. They are linked to a rest endpoint via the attribute
    * addApiResponses via <my>.rest.xml
---
 .../rest-api/api/exampleApiDefinition.rest.xml     |  16 +++
 framework/rest-api/dtd/rest-api.xsd                |   1 +
 framework/rest-api/servicedef/services.xml         |  13 ++-
 .../apache/ofbiz/ws/rs/core/ResponseStatus.java    |   8 +-
 .../apache/ofbiz/ws/rs/model/ModelApiReader.java   |   3 +-
 .../apache/ofbiz/ws/rs/model/ModelOperation.java   |  41 ++++++-
 .../ofbiz/ws/rs/openapi/OFBizOpenApiReader.java    |  56 ++++++++-
 .../org/apache/ofbiz/ws/rs/util/OpenApiUtil.java   |  76 +++++++++----
 .../org/apache/ofbiz/ws/rs/util/RestApiUtil.java   |  15 ++-
 .../ofbiz/ws/rs/test/RestTestHttpRequest.java      | 125 +++++++++++++++++++++
 .../apache/ofbiz/ws/rs/test/RestTestServices.java  |  27 +++++
 .../apache/ofbiz/ws/rs/util/RestApiUtilTest.java   |   3 +-
 framework/rest-api/testdef/rest-apiTests.xml       |   3 +
 13 files changed, 357 insertions(+), 30 deletions(-)

diff --git a/framework/rest-api/api/exampleApiDefinition.rest.xml 
b/framework/rest-api/api/exampleApiDefinition.rest.xml
index ac3bb836ff..137f56de4f 100644
--- a/framework/rest-api/api/exampleApiDefinition.rest.xml
+++ b/framework/rest-api/api/exampleApiDefinition.rest.xml
@@ -31,6 +31,22 @@ under the License.
         </operation>
     </resource>
 
+    <!-- TEST-services -->
+    <!-- The exposed endpoints are neccessary for tests defined in 
RestTestHttpRequest.java to run.-->
+    <!-- If you decide to remove / not publish this rest.xml, those tests need 
to be commented out. -->
+
+    <resource name="orders" path="/returnSuccess">
+        <operation verb="post" consumes="application/json" 
produces="application/json">
+            <service name="returnSuccess"/>
+        </operation>
+    </resource>
+
+    <resource name="orders" path="/returnSuccessButOverwriteStatusCode">
+        <operation verb="post" consumes="application/json" 
produces="application/json">
+            <service name="returnSuccessButOverwriteStatusCode"/>
+        </operation>
+    </resource>
+
     <mapping name="RestOrderExample" 
className="org.apache.ofbiz.ws.rs.examples.RestOrderExample"/>
 
 </api>
\ No newline at end of file
diff --git a/framework/rest-api/dtd/rest-api.xsd 
b/framework/rest-api/dtd/rest-api.xsd
index 44c28948a3..f193cff990 100644
--- a/framework/rest-api/dtd/rest-api.xsd
+++ b/framework/rest-api/dtd/rest-api.xsd
@@ -81,6 +81,7 @@ under the License.
             <xs:attribute name="path" type="xs:string" use="optional"/>
             <xs:attribute name="description" type="xs:string"/>
             <xs:attribute name="auth" type="xs:boolean" default="true"/>
+            <xs:attribute name="addApiResponses" type="xs:string"/>
         </xs:complexType>
     </xs:element>
     <xs:element name="service">
diff --git a/framework/rest-api/servicedef/services.xml 
b/framework/rest-api/servicedef/services.xml
index 0f91c1a509..ba0eb06b60 100644
--- a/framework/rest-api/servicedef/services.xml
+++ b/framework/rest-api/servicedef/services.xml
@@ -47,5 +47,16 @@ under the License.
             location="org.apache.ofbiz.ws.rs.test.RestTestServices" 
invoke="returnCustomErrorTest">
         <description>TestService that returns a custom errorCode</description>
     </service>
-            
+
+    <service name="returnSuccess" engine="java"
+            location="org.apache.ofbiz.ws.rs.test.RestTestServices" 
invoke="returnSuccess">
+        <description>TestService that returns success, but overwrites it with 
a custom http status code</description>
+    </service>
+
+    <service name="returnSuccessButOverwriteStatusCode" engine="java"
+            location="org.apache.ofbiz.ws.rs.test.RestTestServices" 
invoke="returnSuccessButOverwriteStatusCode">
+        <description>TestService that returns success, but overwrites it with 
a custom http status code</description>
+        <attribute name="httpResponseStatus" type="java.lang.Integer" 
mode="OUT" optional="false"/>
+    </service>
+
 </services>
diff --git 
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/core/ResponseStatus.java
 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/core/ResponseStatus.java
index d85791f280..4e1e95268d 100644
--- 
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/core/ResponseStatus.java
+++ 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/core/ResponseStatus.java
@@ -29,7 +29,13 @@ public final class ResponseStatus {
          * 
"https://tools.ietf.org/html/rfc4918#section-11.2";>https://tools.ietf.org/html/rfc4918#section-11.2</a>
          */
         UNPROCESSABLE_ENTITY(422, "Unprocessable Entity");
-
+        /** ============ Custom http Responses START =======
+         *
+         * List Custom Status Codes comma-separated here
+         * MY_CUSTOM_STATUS_CODE(999, "Custom Status Code");
+         *
+         * ============ Custom http Responses END =========
+         */
         private final int code;
         private final String reason;
         private final Family family;
diff --git 
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApiReader.java
 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApiReader.java
index e8987b4962..72f5c3a1be 100644
--- 
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApiReader.java
+++ 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApiReader.java
@@ -122,7 +122,8 @@ public final class ModelApiReader {
                         
.produces(UtilXml.checkEmpty(operationEle.getAttribute("produces")).intern())
                         
.consumes(UtilXml.checkEmpty(operationEle.getAttribute("consumes")).intern())
                         
.description(UtilXml.checkEmpty(operationEle.getAttribute("description")).intern())
-                        
.auth(Boolean.parseBoolean(UtilXml.checkEmpty(operationEle.getAttribute("auth")).intern()));
+                        
.auth(Boolean.parseBoolean(UtilXml.checkEmpty(operationEle.getAttribute("auth")).intern()))
+                        
.addApiResponses(UtilXml.checkEmpty(operationEle.getAttribute("addApiResponses")).intern());
                 resource.addOperation(op);
             } else {
                 Debug.logWarning("Error during creation of ModelApi, due to 
missing 'service' Attribute in ApiModelXml for"
diff --git 
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelOperation.java
 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelOperation.java
index 19ece9f210..995c52a71b 100644
--- 
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelOperation.java
+++ 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelOperation.java
@@ -17,6 +17,11 @@
  * under the License.
  
*******************************************************************************/
 package org.apache.ofbiz.ws.rs.model;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.ofbiz.base.util.StringUtil;
+import org.apache.ofbiz.base.util.UtilValidate;
 
 public class ModelOperation {
 
@@ -27,6 +32,7 @@ public class ModelOperation {
     private String path;
     private String description;
     private boolean auth;
+    private String addApiResponses;
 
     /**
      * Returns whether the user is authenticated.
@@ -46,6 +52,39 @@ public class ModelOperation {
         this.auth = auth;
     }
 
+    /**
+     * @return the addApiResponses
+     */
+    public String getAddApiResponses() {
+        return addApiResponses;
+    }
+
+    /**
+     * @param addApiResponses the addApiResponses to set
+     */
+    public void setAddApiResponses(String addApiResponses) {
+        this.addApiResponses = addApiResponses;
+    }
+
+    /**
+     * @param addApiResponses the addApiResponses to set
+     */
+    public ModelOperation addApiResponses(String addApiResponses) {
+        this.addApiResponses = addApiResponses;
+        return this;
+    }
+
+    /**
+     * @return the addApiResponses as list
+     */
+    public List<String> getAddApiResponsesList() {
+        if (UtilValidate.isEmpty(addApiResponses)) {
+            return new ArrayList<>();
+        }
+        return StringUtil.split(addApiResponses, ",");
+    }
+
+
     /**
      * Sets whether this operation requires JWT authentication and returns
      * this instance.
@@ -235,7 +274,7 @@ public class ModelOperation {
     @Override
     public String toString() {
         return "service: " + service + ", path: " + path + ", verb: " + verb + 
", description: " + description
-                + ", produces: " + produces;
+                + ", produces: " + produces + ", addApiResponses:" + 
addApiResponses;
     }
 
 }
diff --git 
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiReader.java
 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiReader.java
index 48a934daf3..6c45403d7f 100644
--- 
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiReader.java
+++ 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiReader.java
@@ -32,6 +32,7 @@ import org.apache.ofbiz.service.ModelParam;
 import org.apache.ofbiz.service.ModelService;
 import org.apache.ofbiz.webapp.WebAppUtil;
 import org.apache.ofbiz.ws.rs.core.OFBizApiConfig;
+import org.apache.ofbiz.ws.rs.core.ResponseStatus;
 import org.apache.ofbiz.ws.rs.listener.ApiContextListener;
 import org.apache.ofbiz.ws.rs.model.ModelApi;
 import org.apache.ofbiz.ws.rs.model.ModelMapping;
@@ -65,6 +66,7 @@ import jakarta.servlet.ServletContext;
 import jakarta.ws.rs.HttpMethod;
 import jakarta.ws.rs.core.HttpHeaders;
 import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.Response.StatusType;
 
 public final class OFBizOpenApiReader extends Reader implements OpenApiReader {
     private static final String MODULE = OFBizOpenApiReader.class.getName();
@@ -187,8 +189,9 @@ public final class OFBizOpenApiReader extends Reader 
implements OpenApiReader {
             }
 
             addServiceOutSchema(service);
-            addServiceInSchema(service);
+            addServiceInSchema(service, op);
             addServiceOperationApiResponses(service, operation);
+            addAdditionalOperationApiResponses(service, op, operation);
             setPathItemOperation(pathItemObject, verb.toUpperCase(), 
operation);
 
             if (!pathExists) {
@@ -302,8 +305,12 @@ public final class OFBizOpenApiReader extends Reader 
implements OpenApiReader {
         schemas.put("api.response." + service.getName() + ".success", 
OpenApiUtil.getOutSchema(service));
     }
 
+    private void addServiceInSchema(ModelService service, ModelOperation op) {
+        schemas.put("api.request." + service.getName(), 
OpenApiUtil.getInSchema(service, op));
+    }
+
     private void addServiceInSchema(ModelService service) {
-        schemas.put("api.request." + service.getName(), 
OpenApiUtil.getInSchema(service));
+        schemas.put("api.request." + service.getName(), 
OpenApiUtil.getInSchema(service, null));
     }
 
     private void addPredefinedSchemas() {
@@ -322,4 +329,49 @@ public final class OFBizOpenApiReader extends Reader 
implements OpenApiReader {
         operation.setResponses(apiResponsesObject);
     }
 
+    private void addAdditionalOperationApiResponses(ModelService service, 
ModelOperation op, Operation operation) {
+        ApiResponses apiResponsesObject = operation.getResponses();
+
+        if (apiResponsesObject == null) {
+            apiResponsesObject = new ApiResponses();
+        }
+
+        final ApiResponses apiResponsesObjectCopy = apiResponsesObject;
+        op.getAddApiResponsesList().forEach((statusCode) -> {
+
+            StatusType statusType = 
Response.Status.fromStatusCode(Integer.valueOf(statusCode));
+            if (statusType == null) {
+                statusType = 
ResponseStatus.Custom.fromStatusCode(Integer.valueOf(statusCode));
+            }
+
+            if (statusType != null) {
+                String schemaName = "";
+                ApiResponse customResponse = 
OpenApiUtil.getCustomApiResponseByStatusCode(statusCode);
+                if (customResponse != null) {
+                    apiResponsesObjectCopy.addApiResponse(statusCode, 
customResponse);
+                    Schema<?> schema = customResponse.getContent()
+                            .get(jakarta.ws.rs.core.MediaType.APPLICATION_JSON)
+                            .getSchema();
+
+                    String ref = schema.get$ref();
+                    schemaName = ref.substring(ref.lastIndexOf('/') + 1);
+                } else {
+                    schemaName = 
"api.response.service.".concat(service.getName()).concat(".").concat(String.valueOf(statusType.getStatusCode()));
+
+                    ApiResponse response = new 
ApiResponse().description(statusType.getReasonPhrase()).content(new 
Content().addMediaType(
+                            javax.ws.rs.core.MediaType.APPLICATION_JSON,
+                            new MediaType().schema(new 
Schema<>().$ref("#/components/schemas/" + schemaName))));
+                    apiResponsesObjectCopy.addApiResponse(statusCode, 
response);
+                }
+
+                if (statusType.getStatusCode() > 399) {
+                    schemas.put(schemaName, 
OpenApiUtil.getGenericErrorSchema(null));
+                } else {
+                    schemas.put(schemaName, OpenApiUtil.getOutSchema(service));
+                }
+
+            }
+        });
+    }
+
 }
diff --git 
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/util/OpenApiUtil.java 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/util/OpenApiUtil.java
index 4a3897d0aa..9c060622db 100644
--- 
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/util/OpenApiUtil.java
+++ 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/util/OpenApiUtil.java
@@ -40,6 +40,7 @@ import org.apache.ofbiz.webapp.WebAppUtil;
 import org.apache.ofbiz.ws.rs.common.AuthenticationScheme;
 import org.apache.ofbiz.ws.rs.core.ResponseStatus;
 import org.apache.ofbiz.ws.rs.listener.ApiContextListener;
+import org.apache.ofbiz.ws.rs.model.ModelOperation;
 
 import io.swagger.v3.oas.models.headers.Header;
 import io.swagger.v3.oas.models.media.ArraySchema;
@@ -67,9 +68,16 @@ public final class OpenApiUtil {
     private static final Map<String, String> LIST_TYPE_MAP = new HashMap<>();
     private static final Map<String, String> CLASS_ALIAS = new HashMap<>();
     private static final Map<String, Class<?>> JAVA_OPEN_API_MAP = new 
HashMap<>();
-    private static final Map<String, String> FIELD_TYPE_MAP = new 
HashMap<String, String>();
+    private static final Map<String, String> FIELD_TYPE_MAP = new HashMap<>();
     private static final Map<String, ApiResponse> RESPONSES = new HashMap<>();
     private static final Map<String, Schema<?>> SCHEMAS = new HashMap<>();
+    private static final Map<String, ApiResponse> CUSTOM_RESPONSES = new 
HashMap<>();
+    private static final Schema<?> GENERIC_ERROR_SCHEMA = new MapSchema()
+            .addProperty("statusCode", new IntegerSchema().description("HTTP 
Status Code"))
+            .addProperty("statusDescription", new 
StringSchema().description("HTTP Status Code Description"))
+            .addProperty("errorType", new StringSchema().description("Error 
Type for the error"))
+            .addProperty("errorMessage", new StringSchema().description("Error 
Message"));
+
 
     static {
         CLASS_ALIAS.put("String", "String");
@@ -161,21 +169,45 @@ public final class OpenApiUtil {
 
         buildApiResponseSchemas();
         buildApiResponses();
+        builCustomApiResponses();
     }
 
     private static void buildApiResponseSchemas() {
-        Schema<?> genericErrorSchema = new MapSchema();
-        genericErrorSchema.addProperty("statusCode", new 
IntegerSchema().description("HTTP Status Code"));
-
-        genericErrorSchema.addProperty("statusDescription", new 
StringSchema().description("HTTP Status Code Description"));
-        genericErrorSchema.addProperty("errorType", new 
StringSchema().description("Error Type for the error"));
-        genericErrorSchema.addProperty("errorMessage", new 
StringSchema().description("Error Message"));
-        SCHEMAS.put("api.response.unauthorized.noheader", genericErrorSchema);
-        SCHEMAS.put("api.response.unauthorized.invalidtoken", 
genericErrorSchema);
-        SCHEMAS.put("api.response.forbidden", genericErrorSchema);
-        SCHEMAS.put("api.response.service.badrequest", genericErrorSchema);
-        SCHEMAS.put("api.response.service.unprocessableentity", 
genericErrorSchema);
-        SCHEMAS.put("api.response.service.methodnotallowed", 
genericErrorSchema);
+        SCHEMAS.put("api.response.unauthorized.noheader", 
GENERIC_ERROR_SCHEMA);
+        SCHEMAS.put("api.response.unauthorized.invalidtoken", 
GENERIC_ERROR_SCHEMA);
+        SCHEMAS.put("api.response.forbidden", GENERIC_ERROR_SCHEMA);
+        SCHEMAS.put("api.response.service.badrequest", GENERIC_ERROR_SCHEMA);
+        SCHEMAS.put("api.response.service.unprocessableentity", 
GENERIC_ERROR_SCHEMA);
+        SCHEMAS.put("api.response.service.methodnotallowed", 
GENERIC_ERROR_SCHEMA);
+    }
+
+    /*
+     * Define your custom ApiResponses here
+     */
+    private static void builCustomApiResponses() {
+        /**
+         * Map<String, Object> customResponseExample = UtilMisc.toMap(
+         *      "statusCode", 
ResponseStatus.Custom.MY_CUSTOM_STATUS_CODE.getStatusCode(),
+         *      "statusDescription", 
ResponseStatus.Custom.MY_CUSTOM_STATUS_CODE.getReasonPhrase(),
+         *      "errorType", "An Error Type",
+         *      "errorCode", "1234",
+         *      "errorMessage", "Something went Wrong.",
+         *      "errorDescription", "A description of what went wrong.");
+         *
+         * final ApiResponse customResponse = new ApiResponse()
+         * .description("Custom Error: A description of said error.")
+         *       .content(new Content()
+         *               
.addMediaType(jakarta.ws.rs.core.MediaType.APPLICATION_JSON, new MediaType()
+         *                       .schema(new Schema<>()
+         *                               .$ref("#/components/schemas/" + 
"api.response.custom.response.example"))
+         *                       .example(customResponseExample)));
+         *
+         * 
CUSTOM_RESPONSES.put(String.valueOf(ResponseStatus.Custom.MY_CUSTOM_STATUS_CODE.getStatusCode()),
 customResponse);
+        **/
+    }
+
+    public static ApiResponse getCustomApiResponseByStatusCode(String 
statusCode) {
+        return CUSTOM_RESPONSES.get(statusCode);
     }
 
     /**
@@ -208,12 +240,12 @@ public final class OpenApiUtil {
                 "errorMessage", "Forbidden: Insufficient rights to perform 
this API call.");
         Map<String, Object> badRequestExample = UtilMisc.toMap("statusCode", 
Response.Status.BAD_REQUEST.getStatusCode(),
                 "statusDescription", 
Response.Status.BAD_REQUEST.getReasonPhrase(),
-                "errorType", "ServiceValidationException",
+                "errorType", "ServiceValidationException", "errorCode", "1000",
                 "errorMessage", "createProduct validation failed. The request 
contained invalid information and could not be processed.",
                 "errorDescription", "The following required parameter is 
missing: [IN] [createProduct.internalName]");
         Map<String, Object> unprocessableEntExample = 
UtilMisc.toMap("statusCode", 
ResponseStatus.Custom.UNPROCESSABLE_ENTITY.getStatusCode(),
                 "statusDescription", 
ResponseStatus.Custom.UNPROCESSABLE_ENTITY.getReasonPhrase(),
-                "errorType", "GenericEntityException",
+                "errorType", "GenericEntityException", "errorCode", "2000",
                 "errorMessage", "createProduct execution failed. The request 
contained invalid information and could not be processed.",
                 "errorDescription", "StandardException: A truncation error was 
encountered trying to shrink CHAR 'string' to length 1.");
         Map<String, Object> methodNotAllowedExample = 
UtilMisc.toMap("statusCode", Response.Status.METHOD_NOT_ALLOWED.getStatusCode(),
@@ -316,7 +348,7 @@ public final class OpenApiUtil {
      * @param service the {@link ModelService} for which to build the input 
schema
      * @return an OpenAPI {@link Schema} representing the service request body
      */
-    public static Schema<Object> getInSchema(ModelService service) {
+    public static Schema<Object> getInSchema(ModelService service, 
ModelOperation op) {
         Schema<Object> parentSchema = new Schema<Object>();
         parentSchema.setDescription("In Schema for service: " + 
service.getName() + " request");
         parentSchema.setType("object");
@@ -431,14 +463,20 @@ public final class OpenApiUtil {
         Schema<Object> dataSchema = new Schema<Object>();
         parentSchema.addProperty("data", dataSchema);
         service.getOutParamNamesMap().forEach((name, type) -> {
-            Schema<?> attrSchema = getAttributeSchema(service, 
service.getParam(name));
-            if (attrSchema != null) {
-                dataSchema.addProperty(name, getAttributeSchema(service, 
service.getParam(name)));
+            if (!name.equals(RestApiUtil.RESPONSE_STATUS_KEY)) {
+                Schema<?> attrSchema = getAttributeSchema(service, 
service.getParam(name));
+                if (attrSchema != null) {
+                    dataSchema.addProperty(name, getAttributeSchema(service, 
service.getParam(name)));
+                }
             }
         });
         return parentSchema;
     }
 
+    public static Schema<?> getGenericErrorSchema(ModelService service) {
+        return GENERIC_ERROR_SCHEMA;
+    }
+
     private static boolean isTypeGenericEntityOrGenericValue(String type) {
         if (type == null) {
             return false;
diff --git 
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/util/RestApiUtil.java 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/util/RestApiUtil.java
index d8907ab3eb..a8e2ab5cf0 100644
--- 
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/util/RestApiUtil.java
+++ 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/util/RestApiUtil.java
@@ -41,11 +41,11 @@ import org.apache.ofbiz.ws.rs.core.ResponseStatus;
 import org.apache.ofbiz.ws.rs.response.Error;
 import org.apache.ofbiz.ws.rs.response.Success;
 
-import jakarta.ws.rs.core.Response.StatusType;
 import jakarta.ws.rs.core.MediaType;
 import jakarta.ws.rs.core.MultivaluedMap;
 import jakarta.ws.rs.core.Response;
 import jakarta.ws.rs.core.Response.ResponseBuilder;
+import jakarta.ws.rs.core.Response.StatusType;
 
 public final class RestApiUtil {
 
@@ -58,15 +58,22 @@ public final class RestApiUtil {
     }
 
     /**
-     * Builds a JSON success response with HTTP 200 and the given message and 
data.
+     * Builds a JSON success response with HTTP 200 / CUSTOM-STATUS-CODE and 
the given message and data.
+     * Checks for a custom status code within data. Defaults to HTTP 200 if 
non present.
      *
      * @param message a human-readable success message
      * @param data    the response payload
      * @return a JAX-RS {@link Response} with status 200 and a JSON {@link 
Success} body
      */
     public static Response success(String message, Object data) {
-        Success success = new Success(Response.Status.OK.getStatusCode(), 
Response.Status.OK.getReasonPhrase(), message, data);
-        ResponseBuilder builder = 
Response.status(Response.Status.OK).type(MediaType.APPLICATION_JSON).entity(success);
+        StatusType status = extractResponseCode(data);
+
+        if (status == null) {
+            status = Response.Status.OK;
+        }
+
+        Success success = new Success(status.getStatusCode(), 
status.getReasonPhrase(), message, data);
+        ResponseBuilder builder = 
Response.status(status.getStatusCode()).type(MediaType.APPLICATION_JSON).entity(success);
         String linkHeaderValue = getPaginationLinkHeaderValue(data);
         if (UtilValidate.isNotEmpty(linkHeaderValue)) {
             builder.header("Link", linkHeaderValue);
diff --git 
a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/test/RestTestHttpRequest.java
 
b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/test/RestTestHttpRequest.java
new file mode 100644
index 0000000000..9a1c071717
--- /dev/null
+++ 
b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/test/RestTestHttpRequest.java
@@ -0,0 +1,125 @@
+/*******************************************************************************
+ * 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.ws.rs.test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Base64;
+
+import org.apache.ofbiz.base.util.Debug;
+import org.apache.ofbiz.base.util.HttpClient;
+import org.apache.ofbiz.base.util.HttpClientException;
+import org.apache.ofbiz.base.util.SSLUtil;
+import org.apache.ofbiz.testtools.JunitJupiterTest;
+import org.apache.ofbiz.testtools.JupiterTestHelper;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+@JunitJupiterTest
+class RestTestHttpRequest implements JupiterTestHelper {
+
+    private static final String MODULE = RestTestHttpRequest.class.getName();
+    private static final String BASE_URL = "https://localhost:8443/rest";;
+    private static String accessToken;
+    private static ObjectMapper mapper = new ObjectMapper();
+
+    private static HttpClient initHttpClient() {
+        HttpClient http = new HttpClient();
+        http.followRedirects(true);
+        http.setAllowUntrusted(true);
+        http.setHostVerificationLevel(SSLUtil.getHostCertNoCheck());
+        return http;
+    }
+
+    @BeforeAll
+    public static void init() {
+        String creds = 
Base64.getEncoder().encodeToString(("admin:ofbiz").getBytes());
+
+        HttpClient fetchClient = initHttpClient();
+        fetchClient.setUrl(BASE_URL + "/auth/token");
+        fetchClient.setHeader("Authorization", "Basic " + creds);
+        fetchClient.setHeader("Accept", "application/json");
+        String response = "";
+        try {
+            response = fetchClient.post();
+        } catch (HttpClientException e) {
+            Debug.logError(e, "Error returning rest access token", "MODULE");
+            return;
+        }
+
+        try {
+            JsonNode root = mapper.readTree(response);
+            accessToken = root.get("data").get("access_token").asText();
+        } catch (JsonProcessingException | NullPointerException e) {
+            Debug.logError(e, "Error parsing rest auth response", "MODULE");
+        }
+    }
+
+    @Test
+    void returnSuccessreturnsExpectedStatusCode() throws Exception {
+        HttpClient client = initHttpClient();
+        client.setHeader("Content-Type", "application/json");
+        client.setHeader("Authorization", "Bearer " + accessToken);
+        client.setUrl(BASE_URL + "/exampleApi/returnSuccess");
+
+        String response = "";
+        try {
+            response = client.post();
+        } catch (HttpClientException e) {
+            Debug.logError(e, "Error returning rest access token", "MODULE");
+        }
+        int statusCode = 0;
+        try {
+            JsonNode root = mapper.readTree(response);
+            statusCode = root.get("statusCode").asInt(999999999);
+        } catch (JsonProcessingException | NullPointerException e) {
+            Debug.logError(e, "Error parsing rest auth response", "MODULE");
+        }
+
+        assertEquals(200, statusCode);
+    }
+
+    //Corresponding service overwrites statusCode with 201
+    @Test
+    void returnSuccessOverwriteStatusCode() throws Exception {
+        HttpClient client = initHttpClient();
+        client.setHeader("Content-Type", "application/json");
+        client.setHeader("Authorization", "Bearer " + accessToken);
+        client.setUrl(BASE_URL + 
"/exampleApi//returnSuccessButOverwriteStatusCode");
+
+        String response = "";
+        try {
+            response = client.post();
+        } catch (HttpClientException e) {
+            Debug.logError(e, "Error returning rest access token", "AAAA");
+        }
+        int statusCode = 0;
+        try {
+            JsonNode root = mapper.readTree(response);
+            statusCode = root.get("statusCode").asInt(999999999);
+        } catch (JsonProcessingException | NullPointerException e) {
+            Debug.logError(e, "Error parsing rest auth response", "MODULE");
+        }
+
+        assertEquals(201, statusCode);
+    }
+}
diff --git 
a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/test/RestTestServices.java
 
b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/test/RestTestServices.java
index 948c378bd8..9fa10f7d7f 100644
--- 
a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/test/RestTestServices.java
+++ 
b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/test/RestTestServices.java
@@ -23,6 +23,7 @@ import java.util.Map;
 import org.apache.ofbiz.service.DispatchContext;
 import org.apache.ofbiz.service.ModelService;
 import org.apache.ofbiz.service.ServiceUtil;
+import org.apache.ofbiz.ws.rs.util.RestApiUtil;
 
 public class RestTestServices {
 
@@ -38,4 +39,30 @@ public class RestTestServices {
         result.put(ModelService.ERROR_CODE, 999);
         return result;
     }
+
+    // ============== Status Code Test Services =================== //
+    /**
+     * TestService returning a success
+     *
+     * @param dctx
+     * @param context
+     * @return result
+     */
+    public static Map<String, Object> returnSuccess(DispatchContext dctx, 
Map<String, ? extends Object> context) {
+        Map<String, Object> result = ServiceUtil.returnSuccess();
+        return result;
+    }
+
+    /**
+     * TestService returning a success but explicitly returns status code 201 
instead of default 200
+     *
+     * @param dctx
+     * @param context
+     * @return result
+     */
+    public static Map<String, Object> 
returnSuccessButOverwriteStatusCode(DispatchContext dctx, Map<String, ? extends 
Object> context) {
+        Map<String, Object> result = ServiceUtil.returnSuccess();
+        result.put(RestApiUtil.RESPONSE_STATUS_KEY, 201);
+        return result;
+    }
 }
diff --git 
a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/util/RestApiUtilTest.java
 
b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/util/RestApiUtilTest.java
index d8d79c8296..112fa2d74a 100644
--- 
a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/util/RestApiUtilTest.java
+++ 
b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/util/RestApiUtilTest.java
@@ -48,7 +48,8 @@ public final class RestApiUtilTest {
     @Test
     void testSuccess() {
         String message = "Success";
-        String data = "some data";
+        Map<String, Object> data = new HashMap<>();
+        data.put("dataKey", "dataValue");
 
         Response response = RestApiUtil.success(message, data);
 
diff --git a/framework/rest-api/testdef/rest-apiTests.xml 
b/framework/rest-api/testdef/rest-apiTests.xml
index 0a0d8a8ded..393a7bd642 100644
--- a/framework/rest-api/testdef/rest-apiTests.xml
+++ b/framework/rest-api/testdef/rest-apiTests.xml
@@ -24,4 +24,7 @@ under the License.
     <test-case case-name="rest-api-services-tests">
         <jupiter-test-suite 
class-name="org.apache.ofbiz.ws.rs.test.RestServicesTests"/>
     </test-case>
+    <test-case case-name="rest-api-http-requests">
+        <jupiter-test-suite 
class-name="org.apache.ofbiz.ws.rs.test.RestTestHttpRequest"/>
+    </test-case>
 </test-suite>

Reply via email to