Skip to content

Commit

Permalink
feat(support-for-error-templates): add the support of error templates (
Browse files Browse the repository at this point in the history
…#53)

Exception from the server is significant. From server exception, we can take critical decisions. If the exception message from the server is not self-explanatory then it is hard for a client to respond accordingly.  We noticed that we can improve the message thrown to the client from the server.  Now, the client will register an error template against a particular code.  On throwing an exception, the core library will replace the placeholder using JsonPointer.  The user will get the proper message that he wanted.

closes #52
  • Loading branch information
hamzashoukat94 authored Jan 25, 2023
1 parent 551a27d commit 93d8e7d
Show file tree
Hide file tree
Showing 5 changed files with 555 additions and 34 deletions.
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@
<version>0.8.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.json</artifactId>
<version>1.1.2</version>
</dependency>
</dependencies>

<build>
Expand Down
137 changes: 134 additions & 3 deletions src/main/java/io/apimatic/core/ErrorCase.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
package io.apimatic.core;

import java.io.StringReader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.json.Json;
import javax.json.JsonException;
import javax.json.JsonPointer;
import javax.json.JsonReader;
import javax.json.JsonStructure;
import io.apimatic.core.types.CoreApiException;
import io.apimatic.coreinterfaces.http.Context;
import io.apimatic.coreinterfaces.http.HttpHeaders;
import io.apimatic.coreinterfaces.http.response.Response;
import io.apimatic.coreinterfaces.type.functional.ExceptionCreator;

/**
Expand All @@ -19,6 +29,11 @@ public final class ErrorCase<ExceptionType extends CoreApiException> {
*/
private String reason;

/**
* Error message a template or not.
*/
private boolean isErrorTemplate;

/**
* An instance of {@link ExceptionCreator}.
*/
Expand All @@ -28,10 +43,13 @@ public final class ErrorCase<ExceptionType extends CoreApiException> {
* A private constructor.
* @param reason the exception reason.
* @param exceptionCreator the exceptionCreator.
* @param isErrorTemplate error case for error template.
*/
private ErrorCase(final String reason, final ExceptionCreator<ExceptionType> exceptionCreator) {
private ErrorCase(final String reason, final ExceptionCreator<ExceptionType> exceptionCreator,
boolean isErrorTemplate) {
this.reason = reason;
this.exceptionCreator = exceptionCreator;
this.isErrorTemplate = isErrorTemplate;
}

/**
Expand All @@ -41,6 +59,9 @@ private ErrorCase(final String reason, final ExceptionCreator<ExceptionType> exc
* @throws ExceptionType Represents error response from the server.
*/
public void throwException(Context httpContext) throws ExceptionType {
if (isErrorTemplate) {
replacePlaceHolder(httpContext.getResponse());
}
throw exceptionCreator.apply(reason, httpContext);
}

Expand All @@ -53,10 +74,120 @@ public void throwException(Context httpContext) throws ExceptionType {
* thrown exception.
* @return {@link ErrorCase}.
*/
public static <ExceptionType extends CoreApiException> ErrorCase<ExceptionType> create(
public static <ExceptionType extends CoreApiException> ErrorCase<ExceptionType> setReason(
String reason, ExceptionCreator<ExceptionType> exceptionCreator) {
ErrorCase<ExceptionType> errorCase = new ErrorCase<ExceptionType>(reason, exceptionCreator);
ErrorCase<ExceptionType> errorCase =
new ErrorCase<ExceptionType>(reason, exceptionCreator, false);
return errorCase;
}

/**
* Create the errorcase using the error reason and exception creator functional interface which
* throws the respective exception while throwing.
* @param <ExceptionType> Represents error response from the server.
* @param reason the exception message.
* @param exceptionCreator the functional interface which is responsible to create the server
* thrown exception.
* @return {@link ErrorCase}.
*/
public static <ExceptionType extends CoreApiException> ErrorCase<ExceptionType> setTemplate(
String reason, ExceptionCreator<ExceptionType> exceptionCreator) {
return new ErrorCase<ExceptionType>(reason, exceptionCreator, true);
}

private void replacePlaceHolder(Response response) {
replaceStatusCodeFromTemplate(response.getStatusCode());
replaceHeadersFromTemplate(response.getHeaders());
replaceBodyFromTemplate(response.getBody());
}

private void replaceHeadersFromTemplate(HttpHeaders headers) {
StringBuilder formatter = new StringBuilder(reason);
Matcher matcher = Pattern.compile("\\{(.*?)\\}").matcher(reason);
while (matcher.find()) {
String key = matcher.group(1);
String pointerKey = key;
if (pointerKey.startsWith("$response.header.")) {
pointerKey = pointerKey.replace("$response.header.", "");
String formatKey = String.format("{%s}", key);
int index = formatter.indexOf(formatKey);
pointerKey = pointerKey.toLowerCase();
if (index != -1) {
formatter.replace(index, index + formatKey.length(),
"" + (headers.has(pointerKey) ? headers.value(pointerKey) : ""));
}
}
}
reason = formatter.toString();
}

private void replaceBodyFromTemplate(String responseBody) {
StringBuilder formatter = new StringBuilder(reason);
JsonReader jsonReader = Json.createReader(new StringReader(responseBody));
JsonStructure jsonStructure = null;
try {
jsonStructure = jsonReader.read();
} catch (Exception e) {
// No need to do anything here
}
jsonReader.close();
Matcher matcher = Pattern.compile("\\{(.*?)\\}").matcher(reason);
while (matcher.find()) {
String key = matcher.group(1);
String pointerKey = key;
replaceBodyString(responseBody, formatter, jsonStructure, key, pointerKey);
}
reason = formatter.toString().replaceAll("\"", "");
}

private void replaceBodyString(String responseBody, StringBuilder formatter,
JsonStructure jsonStructure, String key, String pointerKey) {
if (pointerKey.startsWith("$response.body")) {
String formatKey = String.format("{%s}", key);
int index = formatter.indexOf(formatKey);
String toReplaceString = "";
toReplaceString = extractReplacementString(responseBody, jsonStructure, pointerKey,
toReplaceString);
if (index != -1) {
try {

formatter.replace(index, index + formatKey.length(), toReplaceString);
} catch (JsonException ex) {
formatter.replace(index, index + formatKey.length(), "");
}
}
}
}

private String extractReplacementString(String responseBody, JsonStructure jsonStructure,
String pointerKey, String toReplaceString) {
if (pointerKey.contains("#")) {
pointerKey = pointerKey.replace("$response.body#", "");
JsonPointer jsonPointer = Json.createPointer(pointerKey);
if (jsonStructure != null && jsonPointer.containsValue(jsonStructure)) {
toReplaceString = jsonPointer.getValue(jsonStructure).toString();
}
} else {
if (responseBody != null && !responseBody.isEmpty()) {
toReplaceString = responseBody;
}
}
return toReplaceString;
}

private void replaceStatusCodeFromTemplate(int statusCode) {
StringBuilder formatter = new StringBuilder(reason);
Matcher matcher = Pattern.compile("\\{(.*?)\\}").matcher(reason);
while (matcher.find()) {
String key = matcher.group(1);
if (key.equals("$statusCode")) {
String formatKey = String.format("{%s}", key);
int index = formatter.indexOf(formatKey);
if (index != -1) {
formatter.replace(index, index + formatKey.length(), "" + statusCode);
}
}
}
reason = formatter.toString();
}
}
49 changes: 31 additions & 18 deletions src/main/java/io/apimatic/core/ResponseHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.apimatic.core.types.CoreApiException;
import io.apimatic.coreinterfaces.compatibility.CompatibilityFactory;
import io.apimatic.coreinterfaces.http.Context;
Expand Down Expand Up @@ -105,13 +107,12 @@ private ResponseHandler(final Map<String, ErrorCase<ExceptionType>> localErrorCa
* @throws ExceptionType Represents error response from the server.
*/
@SuppressWarnings("unchecked")
public ResponseType handle(
Request httpRequest, Response httpResponse, GlobalConfiguration globalConfiguration,
public ResponseType handle(Request httpRequest, Response httpResponse,
GlobalConfiguration globalConfiguration,
CoreEndpointConfiguration endpointConfiguration) throws IOException, ExceptionType {

Context httpContext =
globalConfiguration.getCompatibilityFactory().createHttpContext(httpRequest,
httpResponse);
Context httpContext = globalConfiguration.getCompatibilityFactory()
.createHttpContext(httpRequest, httpResponse);
// invoke the callback after response if its not null
if (globalConfiguration.getHttpCallback() != null) {
globalConfiguration.getHttpCallback().onAfterResponse(httpContext);
Expand Down Expand Up @@ -165,9 +166,8 @@ private <T> T applyDeserializer(Deserializer<T> deserializer, Response httpRespo
}

@SuppressWarnings("unchecked")
private <T> ResponseType createResponseClassType(
Response httpResponse, GlobalConfiguration coreConfig, boolean hasBinaryResponse)
throws IOException {
private <T> ResponseType createResponseClassType(Response httpResponse,
GlobalConfiguration coreConfig, boolean hasBinaryResponse) throws IOException {
CompatibilityFactory compatibilityFactory = coreConfig.getCompatibilityFactory();
switch (responseClassType) {
case API_RESPONSE:
Expand All @@ -187,8 +187,8 @@ private <T> ResponseType createResponseClassType(
}

@SuppressWarnings("unchecked")
private ResponseType createDynamicResponse(
Response httpResponse, CompatibilityFactory compatibilityFactory) {
private ResponseType createDynamicResponse(Response httpResponse,
CompatibilityFactory compatibilityFactory) {
return (ResponseType) compatibilityFactory.createDynamicResponse(httpResponse);
}

Expand All @@ -203,19 +203,32 @@ private void validateResponse(Context httpContext) throws ExceptionType {
int statusCode = response.getStatusCode();
String errorCode = String.valueOf(statusCode);

if (localErrorCases != null && localErrorCases.containsKey(errorCode)) {
localErrorCases.get(errorCode).throwException(httpContext);
}

if (globalErrorCases != null && globalErrorCases.containsKey(errorCode)) {
globalErrorCases.get(errorCode).throwException(httpContext);
}
throwConfiguredException(localErrorCases, errorCode, httpContext);
throwConfiguredException(globalErrorCases, errorCode, httpContext);

if ((statusCode < MIN_SUCCESS_CODE) || (statusCode > MAX_SUCCESS_CODE)) {
globalErrorCases.get(ErrorCase.DEFAULT).throwException(httpContext);
}
}

private void throwConfiguredException(Map<String, ErrorCase<ExceptionType>> errorCases,
String errorCode, Context httpContext) throws ExceptionType {
String defaultErrorCode = "";
Matcher match = Pattern.compile("^[(4|5)[0-9]]{3}").matcher(errorCode);
if (match.find()) {
defaultErrorCode = errorCode.charAt(0) + "XX";
}
if (errorCases != null) {
if (errorCases.containsKey(errorCode)) {
errorCases.get(errorCode).throwException(httpContext);
}
if (errorCases.containsKey(defaultErrorCode)) {
errorCases.get(defaultErrorCode).throwException(httpContext);
}
}
}

public static class Builder<ResponseType, ExceptionType extends CoreApiException> {
/**
* A map of end point level errors.
Expand Down Expand Up @@ -258,8 +271,8 @@ public static class Builder<ResponseType, ExceptionType extends CoreApiException
* @param errorCase to generate the SDK Exception.
* @return {@link ResponseHandler.Builder}.
*/
public Builder<ResponseType, ExceptionType> localErrorCase(
String statusCode, ErrorCase<ExceptionType> errorCase) {
public Builder<ResponseType, ExceptionType> localErrorCase(String statusCode,
ErrorCase<ExceptionType> errorCase) {
if (this.localErrorCases == null) {
this.localErrorCases = new HashMap<String, ErrorCase<ExceptionType>>();
}
Expand Down
Loading

0 comments on commit 93d8e7d

Please sign in to comment.