As the commerce-driven RESTful web services implementation in SAP Commerce Cloud, OCC(Omni Commerce Connect) has been widely used as an integration way with any third party system.
In the real application of OCC, you must consider how to process errors to support different exceptional scenarios, fortunately, OCC has provided an efficient exception handling mechanism, which will be talked in detail next.
Core Components in OCC Exception Handling Mechanism
OCC exception handling is composed of the following key components:
-
- RestExceptionResolver: the entry-point for exception handling, which will determine how to process the thrown exception.
-
- WebserviceErrorFactory: used to convert exception to ErrorWsDTO objects by using a list of predefined ErrorConverts
-
- ErrorConverter: each ErrorConverter is responsible for converting one or a list of specific exceptions to a list of ErrorWsDTO instances.
-
- HttpMessageConverter: each HttpMessageConverter is responsible for marshaling a list of ErrorWsDTO instances to specific format, such as XML, JSON, etc.
As the above diagram shows, whenever an exception is thrown from a OCC controller while it’s servicing an OCC request, RestExceptionResolver will be invoked to process this exception as the entry point
RestExceptionResolver
RestExceptionResolver will determine how to process the thrown exceptions, it will work as follows:
-
- First, setting response status code based on current exception, the default value is 400, you can configure new value.
-
- Next, if logstack is enabled, exception stack will be logged.
-
- Then delegate WebserviceErrorFactory to convert the thrown exception to a list of ErrorWsDTO objects, furthermore, RestExceptionResolver will also decide how to format the error message based on messageFormatterType
-
- finally, invoke suitable HttpMessageConverter to marshal a list of ErrorWsDTO objects to desired response format, such as XML or JSON
WebserviceErrorFactory
WebserviceErrorFactory is used to convert exceptions to ErrorWsDTO objects with a list of configured error converters. The basic logic is simple, each error converter can process specific type of errors (exceptions), so as WebserviceErrorFactory is invoked, it will scan configured error converters one by one, if some error converter can process current error, it will be used to convert this error to ErrorWsDTO object.
By default, OCC implementation has defined some error converters, all of them extends from AbstractErrorConverter, which contains two important methods
-
-
boolean supports(Class clazz this method is used to decide whether current error converter can process passed error, which type is indicated by clazz. Returning true means current error converter can process passed error.
-
-
-
void populate(final SOURCE source, final TARGET target this method is used to convert passed source(error) to target (ErrorWsDTO) .
-
The relationship between preconfigured error converters and the errors they can process are depicted by the following diagram:
Customization
There are two ways to customize OCC exception handling as indicated by the above diagram:
(1) In most cases, if you want to throw an exception containing additional information about an error, which will become part of RESTFul response body, just extend the WebserviceException and provide information about the <type> of the error. Additionally, you can provide the <subjectType> and <subject> in order to expose other information. all subclasses of WebserviceException will be processed by system provided WebserviceExceptionConverter.
(2) If you want to expose a list of errors, for example validation errors, as a single result, several items are required.
-
- Develop a new class to represent related error
-
- Develop a new collection class, which will contain all errors
-
- Develop a new error converter to process this new collection class
Configuration
There are four properties we can configure In OCC exception handling:
(1) logstack: used to decide whether to log the exception stack, the possible value is true and false, as the value is true, the exception stack will be logged:
(2) status: the RESTFul response status code, this property is used to setup the mapping between exception and RESTFul response status code, the default value is 400
(3) messageFormatterType: this property has two possible values
-
- FORWARD: The response error message returned by WebserviceErrorFactory remains unchanged
-
- GENERIC: the value of the message property is used to replace the one returned by WebserviceErrorFactory
(4) message: the value for this property will be used as the generic exception message if the value for messageFormatterType is GENERIC
There are three configuration properties patterns with priority from high to low
-
- the properties with extension name and exception name scope
webservicescommons.resthandlerexceptionresolver.{extensionName}.{exceptionName}.status=401
-
- only the properties with exception name scope
webservicescommons.resthandlerexceptionresolver.{exceptionName}.status=403
-
- the fallback values with deafult scope
webservicescommons.resthandlerexceptionresolver.DEFAULT.status=400
Example
It’s time to demonstrate how to use OCC exception handling by an example. Suppose we’ve implemented a new OCC controller, which will return the average product review rating for specific customer, the code segment looks like this:
...
@Controller
...
public class ProductReviewController
{
...
@RequestMapping(value = "/{userId}/", method = RequestMethod.GET)
...
public ResponseEntity<Double> getAverageReviewRating(@Parameter(description = "customer id", required = true)
@PathVariable(value = "userId")
final String customerId)
{
final CustomerModel customer = (CustomerModel) userService.getUserForUID(customerId);
final Double averageReviewRating = productReviewService.getAverageReviewRating(customer);
return new ResponseEntity<Double>(averageReviewRating, HttpStatus.OK);
}
...
}
Apparently, it will work for happy path, but how about with the following new requirements:
-
- if there is no any product review for current customer, an exception should be thrown and the OCC client should get related message as part of the response body
-
- The OCC client can get the average product review rating only as two conditions are met: the client id must end with hybris.com, the total product review number must be at least three
Next, let’s change our code to support these two new requirements
CustomerReviewExcepton
First, we will introduce a new exception: CustomerReviewException, which will be thrown if there is no any product review for current customer.
...
public class CustomerReviewException extends WebserviceException
{
public static final String NO_REVIEW = "noReviews";
private static final String TYPE = "ReviewRatingError";
private static final String SUBJECT_TYPE = "review";
public CustomerReviewException(final String message)
{
super(message);
}
public CustomerReviewException(final String message, final String reason)
{
super(message, reason);
}
public CustomerReviewException(final String message, final String reason, final Throwable cause)
{
super(message, reason, cause);
}
public CustomerReviewException(final String message, final String reason, final String subject)
{
super(message, reason, subject);
}
@Override
public String getType()
{
return TYPE;
}
@Override
public String getSubjectType()
{
return SUBJECT_TYPE;
}
}
Please note, CustomerReviewException must be a subclass for WebserviceException
For the second requirement, it’s possible to have multiple validation errors, so we need to implement two classes to respectively represent validation error and validation error list. Furthermore, we also need to develop a special error converter to convert this validation error list to a list of ErrorWsDTO objects, in the implementation, it will delegate another converter to finish the concrete work.
CustomerReviewValidationData and CustomerReviewValidationDataList
CustomerReviewValidationData is used to represent validation error:
...
public class CustomerReviewValidationData implements Serializable
{
private static final long serialVersionUID = 1L;
private String message;
private String reason;
public CustomerReviewValidationData() { }
public CustomerReviewValidationData(final String reason, final String message)
{
this.reason = reason;
this.message = message;
}
public String getReason()
{
return reason;
}
public String getMessage()
{
return message;
}
}
CustomerReviewValidationDataList is used to represent a list of validation errors
...
public class CustomerReviewValidationDataList implements Serializable
{
private static final long serialVersionUID = 1L;
private List<CustomerReviewValidationData> customerReviewValidationDataList;
public CustomerReviewValidationDataList() { }
public void setCustomerReviewValidationDataList(final List<CustomerReviewValidationData> customerReviewValidationDataList)
{
this.customerReviewValidationDataList = customerReviewValidationDataList;
}
public List<CustomerReviewValidationData> getCustomerReviewValidationDataList()
{
return customerReviewValidationDataList;
}
}
CustomerReviewValidationErrorConverter and
CustomerReviewValidationListErrorConverter
CustomerReviewValidationErrorConverter is used to convert CustomerReviewValidationData to ErrorWsDTO
...
public class CustomerReviewValidationErrorConverter extends AbstractConverter<CustomerReviewValidationData, ErrorWsDTO>
{
private static final String TYPE = "customerReviewError";
private static final String SUBJECT_TYPE = "customerReview";
private static final String SUBJECT = "customerReviewNumber";
public CustomerReviewValidationErrorConverter() {}
@Override
public void populate(final CustomerReviewValidationData customerReviewValidationData, final ErrorWsDTO errorWsDTO)
{
errorWsDTO.setType(TYPE);
errorWsDTO.setSubjectType(SUBJECT_TYPE);
errorWsDTO.setSubject(SUBJECT);
errorWsDTO.setReason(customerReviewValidationData.getReason());
errorWsDTO.setMessage(customerReviewValidationData.getMessage());
}
}
CustomerReviewValidationListErrorConverter is used to convert a list of CustomerReviewValidationData to a list of ErrorWsDTO instances, it will delegate CustomerReviewValidationErrorConverter to finish concrete work.
Since CustomerReviewValidationListErrorConverter is a subclass of AbstractErrorConverter, it must implement supports(Class) method, current implementation indicates CustomerReviewValidationListErrorConverter can be used to process CustomerReviewValidationDataList exception.
...
public class CustomerReviewValidationListErrorConverter extends AbstractErrorConverter
{
private final CustomerReviewValidationErrorConverter customerReviewValidationErrorConverter;
public CustomerReviewValidationListErrorConverter(final CustomerReviewValidationErrorConverter converter)
{
this.customerReviewValidationErrorConverter = converter;
}
@Override
public boolean supports(final Class clazz)
{
return CustomerReviewValidationDataList.class.isAssignableFrom(clazz);
}
@Override
public void populate(final Object o, final List<ErrorWsDTO> webserviceErrorList)
{
final CustomerReviewValidationDataList customerReviewValidationDataList = (CustomerReviewValidationDataList) o;
webserviceErrorList.addAll(getCustomerReviewValidationErrorConverter()
.convertAll(customerReviewValidationDataList.getCustomerReviewValidationDataList()));
}
protected CustomerReviewValidationErrorConverter getCustomerReviewValidationErrorConverter()
{
return customerReviewValidationErrorConverter;
}
}
We also need to configure these two classes in #YOUR_EXTENSION_NAME#-web-spring.xml
<alias alias="customerReviewValidationErrorConverter" name="defaultCustomerReviewValidationErrorConverter" />
<bean name="defaultCustomerReviewValidationErrorConverter" class="my.commerce.eduocc.errors.converters.CustomerReviewValidationErrorConverter">
<property name="targetClass" value="de.hybris.platform.webservicescommons.dto.error.ErrorWsDTO"/>
</bean>
<alias alias="customerReviewValidationListErrorConverter" name="defaultCustomerReviewValidationListErrorConverter" />
<bean name="defaultCustomerReviewValidationListErrorConverter" class="my.commerce.eduocc.errors.converters.CustomerReviewValidationListErrorConverter">
<constructor-arg ref="customerReviewValidationErrorConverter" />
</bean>
Enable CustomerReviewValidationListErrorConverter
In order to make CustomerReviewValidationListErrorConverter work for exception handling, we should also register it into webserviceErrorFactory, which is located in commercewebservices/web/webroot/WEB-INF/config/common/error-config-spring.xml:
<alias alias="webserviceErrorFactory" name="defaultWebserviceErrorFactory" />
<bean id="defaultWebserviceErrorFactory" class="de.hybris.platform.webservicescommons.errors.factory.impl.DefaultWebserviceErrorFactory">
<property name="converters">
<list>
<ref bean="validationErrorConverter" />
<ref bean="cartVoucherValidationListErrorConverter" />
<ref bean="cartModificationDataListErrorConverter" />
<ref bean="customerReviewValidationListErrorConverter" />
<ref bean="webserviceExceptionConverter" />
<ref bean="exceptionConverter" />
</list>
</property>
</bean>
Revised ProductReviewController
The final ProductReviewController looks like this:
...
@Controller
...
public class ProductReviewController
{
...
@RequestMapping(value = "/{userId}/", method = RequestMethod.GET)
...
public ResponseEntity<Double> getAverageReviewRating(@Parameter(description = "customer id", required = true)
@PathVariable(value = "userId")
final String customerId)
{
final CustomerModel customer = (CustomerModel) userService.getUserForUID(customerId);
final Double averageReviewRating = productReviewService.getAverageReviewRating(customer);
if ((averageReviewRating != null) && (averageReviewRating > 0))
{
final List<CustomerReviewValidationData> list = validateCustomerReviewData(customer);
if (!list.isEmpty())
{
final CustomerReviewValidationDataList crValidationDataList = new CustomerReviewValidationDataList();
crValidationDataList.setCustomerReviewValidationDataList(list);
throw new WebserviceValidationException(crValidationDataList);
}
return new ResponseEntity<Double>(averageReviewRating, HttpStatus.OK);
}
else
{
//return new ResponseEntity<Double>(HttpStatus.NO_CONTENT);
throw new CustomerReviewException("no average review rating", CustomerReviewException.NO_REVIEW,
"the average review rating is null");
}
}
...
}
Please note, if there are validation errors, we need to wrap it in a WebserviceValidationException.
Verification
Now we can verify our functionalities to see whether or not these two requirements are met,
(1) Register a new customer, who has no any product review, then invoke this OCC controller with this customer, you will find this in the client side:
(2) Register another customer, whose id doesn’t end with hybris.com. Then setup two product reviews for some products, invoke this OCC controller with this customer, you will find this in the client side: