View Javadoc
1   package com.acumenvelocity.ath.common;
2   
3   import java.io.File;
4   import java.util.HashMap;
5   import java.util.Map;
6   
7   import javax.ws.rs.core.MediaType;
8   import javax.ws.rs.core.Response.Status;
9   
10  import com.acumenvelocity.ath.model.ResponseCodeMessage;
11  
12  import io.swagger.oas.inflector.models.ResponseContext;
13  import net.sf.okapi.common.Util;
14  
15  /**
16   * Utility class for creating standardized HTTP responses in a REST API context.
17   * Provides methods for creating success and error responses with consistent
18   * formatting and content types.
19   * 
20   * <p>
21   * All responses are returned as JSON with UTF-8 encoding and include
22   * appropriate HTTP status codes and logging.
23   * </p>
24   * 
25   * @author Acumen Velocity
26   * @version 1.0
27   * @since 1.0
28   * @see ResponseContext
29   * @see ResponseCodeMessage
30   */
31  public class Response {
32    // Media type constants for consistent content type handling
33    static public final MediaType APPLICATION_JSON_MEDIA_TYPE = new MediaType("application", "json");
34    static public final String APPLICATION_JSON = APPLICATION_JSON_MEDIA_TYPE.toString();
35  
36    static public final MediaType APPLICATION_JSON_UTF8_MEDIA_TYPE = new MediaType("application",
37        "json", "utf-8");
38    static public final String APPLICATION_JSON_UTF8 = APPLICATION_JSON_UTF8_MEDIA_TYPE.toString();
39  
40    /**
41     * Creates a standardized response with the specified status code and message.
42     * Automatically logs the message at appropriate level based on status code.
43     * 
44     * @param code    the HTTP status code (2xx for success, others for errors)
45     * @param message the response message text
46     * @return ResponseContext configured with status, entity, and content type
47     */
48    private static ResponseContext makeResponse(int code, String message) {
49      // Log success responses at INFO level, errors at WARN level
50      if (code >= 200 && code <= 299) {
51        Log.info(Response.class, message);
52      } else {
53        Log.warn(Response.class, message);
54      }
55  
56      // Create response code object and serialize to JSON
57      ResponseCodeMessage rc = new ResponseCodeMessage();
58      rc.setCode(code);
59      rc.setMessage(message);
60      String st = JacksonUtil.toJson(rc, false);
61  
62      // Build and return the response context
63      return new ResponseContext()
64          .status(code)
65          .entity(EncodingUtil.toUtf8(st)) // Ensure UTF-8 encoding
66          .contentType(APPLICATION_JSON_UTF8_MEDIA_TYPE);
67    }
68  
69    /**
70     * Creates an error response with the specified status code and message.
71     * 
72     * @param code    the HTTP error status code (e.g., 400, 404, 500)
73     * @param message the error message text
74     * @return ResponseContext for the error response
75     */
76    public static ResponseContext error(int code, String message) {
77      return makeResponse(code, message);
78    }
79  
80    /**
81     * Creates an error response with formatted message.
82     * 
83     * @param code      the HTTP error status code
84     * @param format    the message format string with placeholders
85     * @param arguments the values for the placeholders
86     * @return ResponseContext for the error response
87     */
88    public static ResponseContext error(int code, String format, Object... arguments) {
89      String message = Log.format(format, arguments);
90      return error(code, message);
91    }
92  
93    /**
94     * Creates an error response with exception details.
95     * 
96     * @param code    the HTTP error status code
97     * @param cause   the exception that caused the error
98     * @param message the error message text
99     * @return ResponseContext for the error response
100    */
101   public static ResponseContext error(int code, Throwable cause, String message) {
102     String st = Log.format("{} -- {}", message, unwindCause(cause));
103     Log.error(Response.class, st);
104     return makeResponse(code, st);
105   }
106 
107   public static String unwindCause(Throwable t) {
108     if (t == null) {
109       return "";
110     }
111 
112     StringBuilder sb = new StringBuilder();
113     Throwable cur = t;
114 
115     while (cur != null) {
116       String msg = cur.getMessage();
117 
118       if (msg != null && !msg.isBlank()) {
119         if (sb.length() > 0) {
120           sb.append(" -- ");
121         }
122 
123         sb.append(msg.trim());
124       }
125 
126       cur = cur.getCause();
127 
128       if (cur == t) {
129         break; // guard against circular cause chains
130       }
131     }
132 
133     return sb.toString();
134   }
135 
136   /**
137    * Creates an error response with exception details and formatted message.
138    * 
139    * @param code      the HTTP error status code
140    * @param cause     the exception that caused the error
141    * @param format    the message format string with placeholders
142    * @param arguments the values for the placeholders
143    * @return ResponseContext for the error response
144    */
145   public static ResponseContext error(int code, Throwable cause, String format,
146       Object... arguments) {
147     return error(code, cause, Log.format(format, arguments));
148   }
149 
150   /**
151    * Creates a success response with formatted message.
152    * 
153    * @param code      the HTTP success status code (e.g., 200, 201)
154    * @param format    the message format string with placeholders
155    * @param arguments the values for the placeholders
156    * @return ResponseContext for the success response
157    */
158   public static ResponseContext success(int code, String format, Object... arguments) {
159     String message = Log.format(format, arguments);
160     return success(code, message);
161   }
162 
163   /**
164    * Creates a success response with an object as the response body.
165    * The object will be serialized to JSON.
166    * 
167    * @param code the HTTP success status code
168    * @param obj  the object to include in the response body
169    * @return ResponseContext for the success response
170    */
171   public static ResponseContext success(int code, Object obj) {
172     return success(code, obj, true);
173   }
174 
175   /**
176    * Creates a success response with an object as the response body.
177    * Provides control over ObjectId serialization format.
178    * 
179    * @param code                   the HTTP success status code
180    * @param obj                    the object to include in the response body
181    * @param forceObjectIdAsObjects if true, ObjectIds are serialized as objects;
182    *                               if false, they are serialized as strings
183    * @return ResponseContext for the success response
184    */
185   public static ResponseContext success(int code, Object obj, boolean forceObjectIdAsObjects) {
186     // Handle string objects separately
187     if (obj instanceof String) {
188       return makeResponse(code, (String) obj);
189     }
190 
191     // Serialize object to JSON with specified ObjectId handling
192     // Note: The current implementation uses the same serializer regardless of the flag
193     // This may need to be updated to use different ObjectMapper instances
194     String st = forceObjectIdAsObjects ? JacksonUtil.toJson(obj, false)
195         : JacksonUtil.toJson(obj, false);
196 
197     return new ResponseContext()
198         .status(code)
199         .entity(EncodingUtil.toUtf8(st))
200         .contentType(APPLICATION_JSON_UTF8_MEDIA_TYPE);
201   }
202 
203   /**
204    * Creates a simple success response with default "Success" message.
205    * 
206    * @param code the HTTP success status code
207    * @return ResponseContext for the success response
208    */
209   public static ResponseContext success(int code) {
210     return makeResponse(code, "Success");
211   }
212 
213   public static ResponseContext makeResponse(Status status, Object obj, String headerKey,
214       String headerValue) {
215 
216     // Handle String and File objects separately
217     if (obj instanceof String) {
218       return makeResponse(status.getStatusCode(), (String) obj);
219 
220     } else if (obj instanceof File) {
221       return new ResponseContext()
222           .status(status)
223           .header(headerKey, headerValue)
224           .contentType(MediaType.APPLICATION_OCTET_STREAM)
225           .entity(obj);
226     }
227 
228     // Any other object
229     String st = JacksonUtil.toJson(obj, false);
230 
231     return Util.isEmpty(headerKey) || Util.isEmpty(headerValue) ? new ResponseContext()
232         .status(status)
233         .entity(EncodingUtil.toUtf8(st)) // Ensure UTF-8 encoding
234         .contentType(Response.APPLICATION_JSON_UTF8_MEDIA_TYPE)
235         : new ResponseContext()
236             .status(status)
237             .header(headerKey, headerValue)
238             .entity(EncodingUtil.toUtf8(st)) // Ensure UTF-8 encoding
239             .contentType(Response.APPLICATION_JSON_UTF8_MEDIA_TYPE);
240   }
241 
242   /**
243    * Creates a builder for constructing a ResponseContext with chained method calls.
244    * 
245    * @return a new Builder instance
246    */
247   public static Builder builder() {
248     return new Builder();
249   }
250 
251   /**
252    * Builder class for creating ResponseContext instances with a fluent API.
253    */
254   public static class Builder {
255     private Status status;
256     private Object entity;
257     private final Map<String, String> headers = new HashMap<>();
258 
259     /**
260      * Sets the HTTP status for the response.
261      * 
262      * @param status the HTTP status
263      * @return this Builder for chaining
264      */
265     public Builder status(Status status) {
266       this.status = status;
267       return this;
268     }
269 
270     /**
271      * Adds a header to the response.
272      * 
273      * @param name  the header name
274      * @param value the header value
275      * @return this Builder for chaining
276      */
277     public Builder header(String name, String value) {
278       if (!Util.isEmpty(name) && !Util.isEmpty(value)) {
279         headers.put(name, value);
280       }
281       return this;
282     }
283 
284     /**
285      * Sets the entity for the response (serialized to JSON for non-String/File types).
286      * 
287      * @param entity the entity object
288      * @return this Builder for chaining
289      */
290     public Builder entity(Object entity) {
291       this.entity = entity;
292       return this;
293     }
294 
295     /**
296      * Builds the ResponseContext with the configured status, headers, and entity.
297      * 
298      * @return the constructed ResponseContext
299      */
300     public ResponseContext build() {
301       if (status == null) {
302         throw new IllegalStateException("Status must be set");
303       }
304 
305       if (entity == null) {
306         return makeResponse(status.getStatusCode(), "Success");
307       }
308 
309       ResponseContext context = new ResponseContext();
310       context.status(status);
311 
312       for (Map.Entry<String, String> header : headers.entrySet()) {
313         context.header(header.getKey(), header.getValue());
314       }
315 
316       if (entity instanceof String) {
317         return makeResponse(status.getStatusCode(), (String) entity);
318 
319       } else if (entity instanceof File) {
320         context.contentType(MediaType.APPLICATION_OCTET_STREAM);
321         context.entity(entity);
322 
323       } else {
324         String st = JacksonUtil.toJson(entity, false);
325         context.entity(EncodingUtil.toUtf8(st));
326         context.contentType(APPLICATION_JSON_UTF8_MEDIA_TYPE);
327       }
328 
329       return context;
330     }
331   }
332 
333   public static String getMessage(ResponseContext res) {
334     ResponseCodeMessage rcm = JacksonUtil.fromJson(res.getEntity().toString(),
335         ResponseCodeMessage.class);
336 
337     return rcm == null ? null : rcm.getMessage();
338   }
339 }