Hybris Extension Architecture Case: Decoupling
1. Introduction
In systems development, particularly in the object-oriented universe, code reuse is a must for maintaining a healthy development ecosystem inside a business unit. For example, in an enterprise with outsourcing basis focused on a specific niche, several projects may use same business logic as, for instance, communication with an outside partner system, like payment gateways and delivery quotation. To reuse code is to spend less time, less resources, less money. This implies in reduced deadlines, competitive budgets and better customer relationship, marketing, etc.
In SAP Hybris environment, this makes a lot more sense with the extensions concept. As one may know, extensions shall be plugged in various SAP Hybris systems and need to perform similarly in all of them, needing only little configuration.
In this article, I’m going to show how I developed an extension that calls a remote partner webservice for delivery quotation, that focus in decoupling and abstraction. I’ll start with some object-oriented concepts, good practice concepts, how I applied them, the idea of the architecture behind the extension and, finally, the pluggin’ into my SAP Hybris e-commerce system – that must always be developed outside the extension.
2. Scenario
One very common need, inside an e-commerce system, is to show to clients the delivery quotation cost and the possible date for delivery for the cart that the client wants to buy.
Here’s the deal: there’s a need to call a remote partner webservice that will send us the quotation for a zip code and the list of items of a cart. Let’s define a name and the concept of this partner webservice:
Equessi – fictitious name – is a partner that offers a series of delivery services, such as order collecting, delivery tracking and statistics service related to the delivery. It offers a webservice that has some endpoints. Among them, the particular ones are: quoting a delivery by products, creating a delivery order, updating the delivery order – such as invoices and other legal information -, and providing tracking information for the delivery.
Now that we know our partner – the details of the webservice implementation and requests and responses will be shown later -, we can start thinking about the concepts to apply to our new extension and its architecture.
3. Extension objectives and responsibilities
For this scenario, the extension objectives and responsibilities are:
- Build requests with the parameters from the Hybris Platform,
- Send these requests to the endpoint of our partner webservice endpoint
- Receive a response and deserialize it to a known object
- Return the response object to the Hybris Platform
Here, we can notice that this extension won’t perform any business logic action. For instance, it won’t change the content of an OrderModel. It will only build requests, send them, wait for a response, deserialize it, and return to Hybris Platform.
If it did some business logic action, it would be coupled to the Hybris Platform being used for one specific project.
4. General architecture concepts
In this section, I’ll review two architecture concepts that are important in code reuse oriented development. They’ll make sense along the path of this article when I’ll describe the extension architecture.
They are: decoupling and abstraction.
4.1. Decoupling
If we intend to reuse code, the layer/class/jar shall not depend on outside codes that cannot be delivery with our implementations. I.e., everything inside our extension must work only with embedded jars and with the language standard classes.
For Java, these classes are the ones from java*. packages. For Hybris, these classes are the ones shipped with the standard Hybris Platform, such as CartModel or OrderModel, without customizations.
In our extension, we avoid even using Hybris Platform classes. We will only use self-defined classes (inside the extension) and classes from dependencies such as fasterxml.jackson, google.gson and org.joda.time.
If we follow this premise, the extension may be plugged into any Hybris Platform (even if there are customizations and code overwriting) because it won’t depend on its objects. For example, we will see here that the extension service layer will only accept java objects and self-defined objects as parameters. So, there won’t be a method, for instance, that receives a CartModel object as parameter, but will accept java.lang.String and EquessiProductEntity objects.
4.2. Abstraction
In computer universe, there’s a famous aphorism known as the Greenspun’s tenth rule of programming that states:
Any sufficiently complicated C or Fortran program contains an ad-hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp.
This quote, even if it sounds a little cheeky, affirms that C and Fortran languages perspectives and functionalities are contained inside the Common Lisp language because of its architecture and abstraction level. Common Lisp was designed to be a multi-paradigm language and, therefore, handles C and Fortran software translated implementations powerfully because of its abstractions.
That said, abstraction, in software engineering, is a principle that could be written as the following:
Any functionality in the software that is used in two or more places shall be placed in one single place, in the form of abstraction, and used by other pieces of code in a transparent way.
For example, we will see that, in the extension that is object of this article, I use an abstraction class to define two widely used methods in all requests that are sent to the webservice: doPost and doGet.
With that, we can reuse code as it was intended to be.
5. Extension definitions and architecture
As for the name of my delivery system integration extension, I chose, for simplicity, the partner name. I.e., my extension is named equessi.
To create the new extension, I run the ant extgen
command and followed the steps choosing the yempty
template/accelerator. After creating it, its structure looks very simple. You should already be familiar to this.
As mentioned before, this extension will serialize and deserialize objects to the JSON standard. Fortunately, a library that is very widely used in Java world is the fasterxml jackson and it is already shipped within the Hybris Platform and its resources can be imported within our extension.
To perform HTTP requests to our partner webservice, we shall use http.components
from the Apache Software Foundation. These need to be declare as an external dependency inside our external-dependencies.xml
file. Here’s what we need to put inside:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.4</version> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpcore</artifactId> <version>4.4.8</version> </dependency> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.3</version> </dependency> |
Note: remember that we need to enable maven in extension-info.xml
with the property usemaven="true"
and run ant updateMavenDependecies
command.
Now that the extension is fairly configured, we may proceed to the architecture idea.
5.1. Equessi extension Architecture overview
Because of the nature of the extension, its responsibilities, objectives, scope, chained with my experience brought from other scenarios, I formulated the following architecture:
- The extension will be service-layer interface based.
- When the extension is plugged into a Hybris Platform, its functionalities shall be offered via this layer. As the extension is pretty simple, we shall see that there will be only one solid service, called EquessiService.java
- There won’t be a facades layer: as the facades layer is commonly used to group services to serve web-based layers, like controllers, they won’t happen here. Another thing: the extension is pretty common and we won’t have many/enough services to build a facade.
- There will be an ‘entities’ package/layer: the layers here in this extension will separated by packages. It eases the reading and the locate process of a class. Since the extension won’t have many classes nor layers, separating layers by packages will give us less paths to search into, and will keep the significance.
The ‘entities’ layer will contain all the entity classes that are part of the serialization and de-serialization of JSON requests and responses. They are coupled to the fasterxml.jackson JSON library by the @JsonProperty annotations.
- Validation Parameter layer: Classes that encapsulate groups of attributes that will be used within the logics of the extension. Instead of passing a large number of attributes as parameters to different methods in different layers, these classes will be used for this purpose and they have a good drive: easy validation.
- Validation Rules layer: Classes (we will see that they are actually beans that will perform stateless operations) that are used to validate such parameters mentioned above. With a simple call, one can validate the parameters calling one of these beans, leaving service layer code clean.
- Selectors layer: this one is a little bit too much… In other terms, it contains beans that select an item from a list of items. Translating: it encapsulates choices as methods like, for instance, getCheapestDeliveryOption or getFastestDeliveryOption. There was not a better way to separate this idea than in a separate auxiliar layer/package, as it is not a part of any other layers (except Service but, you know, code reuse…).
- Suppressor layer: Classes that will suppress/remove certain attributes from the entities to be serialized. This is good if we think that the requisites of our partner system can change. We can remove properties without actually changing the entity model and filter whatever information we want to send to our partner in specific places and requests. For example, CreateShipmentOrderByProducts from EquessiService will use the same EquessiProductEntity as UpdateTrackingData from the same service, but in the second one, we won’t need a lot of pre-populated attributes.
NOTE: this can be a down for performance: populating with extra information and removing isn’t nice. But creating a single build method for each of those entities won’t be fast-development driven.
- Request and Response entities layer: They contain the request descriptor that is going to be sent with requests to our partner webservice and responses that will be de-serialized from the webservice. Maybe we should place it into the ‘entities’ layer as it sure looks like an entity. But here, the idea is to separate them to be more visible since they have a particular responsibility: encapsulate some of others entities to build a request/response object.
5.2. Contants and Enumerations
As our partner specifies the urls that work as endpoints to specific requests and services, its good practice to have them organized in a single enumeration class so, if we want to add something after – a new endpoint for a new service -, we just need to change it there. Additionally, the endpoints urls will be available in a single point to be used by all extension.
The code below exemplifies this enumeration class.
When building the url to send requests, this enumeration will be used.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | public enum EquessiServiceEndpointsEnum { /* * A brief list of the equessi endpoints. * They are all POST services that receives a JSON to perform some action. */ QUOTE_BY_PRODUCTS_LIST("/quote/products"), QUOTE_BY_VOLUMES_LIST("/quote/volumes"), CREATE_SHIPMENT_ORDER_BY_VOLUMES("/order/new/volumes"), CANCEL_SHIPMENT_ORDER("/order/cancel"); private final String endpoint; private EquessiServiceEndpointsEnum(final String endpoint){ this.endpoint = endpoint.intern(); } public String getEndpoint() { return endpoint; } } |
Our partner specifies that we need to have some header information in our requests for authentication. Also, it specifies that for each environment there will be a different access url. As we may know, the Hybris System config file local.properties
is the place for it. Therefore, this file shall contain the following information:
1 2 3 | equessi.api.url=https://webservice.equessi.com/api/v1 equessi.api.user=equessi.test.user equessi.api.password=equessi.test.user.password |
Therefore, we will use these information in our extension getting them from here with the spring property injection into our AbstractEquessiService
that will contain the correlated attributes for these information and will contain strategic methods that will be performed in our concrete DefaultEquessiService
.
The property injection is placed below (this code is place into equessi-spring.xml
):
1 2 3 4 5 6 | <alias alias="equessiService" name="defaultEquessiService" /> <bean id="defaultEquessiService" class="com.gregoreki.equessi.service.impl.DefaultEquessiService"> <property name="equessiServiceUrl" value="${equessi.api.url}" /> <property name="equessiApiKey" value="${equessi.api.key}" /> <!-- other injections here --> </bean> |
5.3. Classes, Interfaces, Entities
As commented before, the extension will be service-layer based. I.e., its features will be used by Hybris Platform through the service layer, specifically the DefaultEquessiService extends AbstractEquessiService implements EquessiService
.
Explaining a little more, DefaultEquessiService
will implement the methods that comes from the interface and will inherit methods from the abstract class. These methods from the abstract class are not abstract: they have a default implementation and serve to get a request object parameter and send it to the Equessi api url. After that, the methods convert the answer into an ‘Object’ that is known to us, and will be cast to the right object class inside the service method that invoked the method from the abstract class.
The conversion from the answer (JSON) to a Object is performed by fasterxml jackson library
. To perform this, there is an ObjectMapper
that parses the response from our partner and converts it into a object of a given class. As said before, this mapper will be a self-instantiated by default configuration inside the abstract service class. I.e., the get method will perform a new operation, if the mapper is null, and will configure it with a hard coded concrete configuration that shall not be changed. The extension is built upon the specifications of this mapper, therefore, it cannot be changed nor injected.
Actually, one can override the method and change its body. However, this is not encouraged.
Now, lets observe our AbstractEquessiService
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | package com.gregoreki.equessi.service; import java.io.IOException; import org.apache.http.HttpResponse; import org.apache.http.ParseException; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.util.EntityUtils; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.core.JsonGenerationException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.google.gson.JsonObject; import com.google.gson.JsonParser; /** * @author c.gregoreki */ public class AbstractEquessiService { private static final Logger LOG = Logger.getLogger(AbstractEquessiService.class); private String equessiServiceUrl; private String equessiApiUser; private String equessiApiPassword; /* * ObjectMapper will be unique for all objects and requests. It's a singleton. * The approach here is to not configure it in *spring.xml because of its idiosyncratic * configuration. * * The getObjectMapper method builds a new one if there's none assigned, fully configured for the task. */ private ObjectMapper objectMapper; protected Object doPostRequest(final Object object, final String url, final Class responseObjectClass) throws JsonGenerationException, JsonMappingException, IOException { final HttpClient httpClient = HttpClientBuilder.create().build(); // convert the request object into a string String str = getObjectMapper().writeValueAsString(object); final HttpPost postRequest = new HttpPost(url); //set user and password for authentication postRequest.setHeader("api-user", getEquessiApiUser()); postRequest.setHeader("api-user", getEquessiApiPassword()); postRequest.setHeader("Content-type", "application/json"); postRequest.setHeader("charset", "utf-8"); // the post request body postRequest.setEntity(new StringEntity(stringObject)); // execute the api call final HttpResponse response = httpClient.execute(postRequest); /* * convert the string that came from the api into a known object * the object class is passed by parameter. */ final Object responseObject = getResponseObject(response, responseObjectClass); // return the parsed object. it can be null if any error happens. return responseObject; } protected Object doGetRequest(final String url, final Class responseObjectClass) throws ClientProtocolException, IOException { /* * for simplicity, we hide this implementation which is very similiar of doPostRequest() */ } protected Object getResponseObject(final HttpResponse response, final Class<Object> objectClass) throws ParseException, IOException { // get the response as string (the answer will always be UTF-8) final String stringResponse = EntityUtils.toString(response.getEntity(), "UTF-8"); final JsonParser parser = new JsonParser(); // parse the response to JSON object final JsonObject obj = parser.parse(stringResponse).getAsJsonObject(); // object should not be null, otherwise the request failed if (obj != null) { // if request was successful, it should contain 'response' if (obj.get("response") != null) { // return response as object return getObjectMapper().readValue(obj.get("response").toString(), objectClass); } // if the status is ERROR, the request failed else if (obj.get("status") != null && obj.get("status").toString().equals(""ERROR"")) { // throw an exception, that contains error message of failed request throw new JsonGenerationException(obj.toString()); } } return null; } /* * This method builds a object mapper with a fixed configuration if it is null on the context of * this class. When this class is a named bean in hybris/spring context, the object will be created only * once. */ protected ObjectMapper getObjectMapper() { if (this.objectMapper == null) { this.objectMapper = new ObjectMapper(); } this.objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); this.objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES); this.objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); this.objectMapper.configure(com.fasterxml.jackson.core.JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); this.objectMapper.setSerializationInclusion(Include.NON_NULL); return this.objectMapper; } /* * getters and setters omitted. */ } |
Now that our abstract class is defined, we have in our hands the doPost method. This method is powerful as it receives a class as parameter and converts the answer of an specified post request to our partner api url into an object of that class.
With that, we can now start to build the methods that are part of our DefaultEquessiService
. Let’s check this class out. The codes of some methods are hidden for simplicity and I will explicit one of them to understand that all the methods can follow one template:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | package com.gregoreki.equessi.service.impl; import java.io.IOException; import java.util.ArrayList; import java.util.List; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.ClientProtocolException; import org.joda.time.LocalDate; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import com.fasterxml.jackson.core.JsonGenerationException; import com.gregoreki.equessi.data.entities.*; import com.gregoreki.equessi.data.request.*; import com.gregoreki.equessi.data.response.*; import com.gregoreki.equessi.data.suppressors.*; import com.gregoreki.equessi.service.AbstractEquessiService; import com.gregoreki.equessi.service.EquessiService; import com.gregoreki.equessi.url.enums.EquessiServiceEndPointsEnum; import com.gregoreki.equessi.validation.exception.EquessiValidationException; import com.gregoreki.equessi.validation.parameter.*; import com.gregoreki.equessi.validation.rules.*; /* * note that how our layers are used here. * The methods inside this class shall have the following template: * create a validationParameter with the parameters that were passed to the method, * then pass this parameter to a validationRule that will validate the presence and * values of the parameter parts. After that, it shall build the request object with the validated * parameter, build the url for that method purpose and, finally, call the doPost method * from the abstract class, casting it to the desired class (that is passed to doPost as parameter). */ /** * @author c.gregoreki */ public class DefaultEquessiService extends AbstractEquessiService implements EquessiService { /* ---------------------------------- * begin: validation rules injections * ---------------------------------- */ private EquessiShipmentQuotationByVolumesValidationRule equessiShipmentQuotationByVolumesValidationRule; private EquessiCreateShipmentOrderValidationRule equessiCreateShipmentOrderValidationRule; /* ------------------------------ * begin: methods implementations * ------------------------------ */ @Override public ShipmentQuotationByVolumesResponse getEquessiDeliveryQuotationByVolumes(String origin_postal_code, String origin_country_ISO, String destination_postal_code, String origin_country_ISO, List<EquessiVolumeEntity> volumes) throws EquessiValidationException, IOException { /* * start validation logic. delegate responsibility to the suitable rule bean. a suitable parameter needs to be * created. */ /* * create a EquessiShipmentQuotationByVolumesValidationParameter with all the parameters. * THIS IS IMPORTANT: note that the method signature only receives java.lang and Equessi objects. * * get the injected EquessiShipmentQuotationByVolumesValidationRule and call the validate method passing * the built parameter to validate it. If anything goes wrong, a EquessiValidationException will be thrown * and the upper layer needs to treat it. * * call a builder method to build the request object ShipmentQuotationByVolumesRequest with the above parameter * */ // builds the URL using the EquessiServiceEndPointsEnum final String finalUrl = new StringBuilder().append(getEquessiServiceUrl()) .append(EquessiServiceEndPointsEnum.QUOTE_BY_VOLUMES_LIST.getEndpoint()).toString(); // call the abstract doPost Method final ShipmentQuotationByVolumesResponse response = (ShipmentQuotationByVolumesResponse) doPostRequest( requestObject, finalUrl, ShipmentQuotationByVolumesResponse.class); return response; } /* * other methods omitted for simplicity. They follow the same template. * getters and setters omitted. */ } |
A good question: what are those ShipmentQuotationByVolumesResponse
and EquessiVolumeEntity
objects? The answer is: they are the entities that are used to serialize and deserialize requests and responses with JSON. The requestObject
listed above is of ShipmentQuotationByVolumesRequest
type, which is one of the requests objects. This object encapsulates a series of other entity objects that encapsulates other entities or primitive/java.lang types.
So, accordingly with the Equessi documentation/specification, we need to send a specific form with required data to create the order into their service. So, below is a example of JSON that needs to be sent and later are the entities and how they connect to each other to form the request.
1 2 3 4 5 6 7 8 9 10 11 | { "origin_postal_code" : "c1001", "origin_country_ISO" : "ARG", "destination_postal_code": "c1009", "origin_country_ISO" : "ARG", "volumes" : [{ "type" : "BOX", "weight" : "23,9", "nature" : "apparel" }] } |
Therefore, we map this request as the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | package com.gregoreki.equessi.data.request; import java.util.List; import com.fasterxml.jackson.annotation.JsonProperty; import com.gregoreki.equessi.data.entity.EquessiVolumeEntity; /** * @author c.gregoreki */ public class ShipmentQuotationByVolumesRequest { @JsonProperty("origin_postal_code") private String originPostalCode; @JsonProperty("origin_country_ISO") private String originCountryIso; @JsonProperty("destination_postal_code") private String destinationPostalCode; @JsonProperty("destination_country_ISO") private String destinationCountryIso; @JsonProperty("volumes") private List<EquessiVolumeEntity> volumes; /* * getters and setters omitted. */ } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | package com.gregoreki.equessi.data.entity; import com.fasterxml.jackson.annotation.JsonProperty; /** * @author c.gregoreki */ public class EquessiVolumeEntity { @JsonProperty("type") private String type; @JsonProperty("weight") private String weight; @JsonProperty("nature") private String nature; /* * getters and setters omitted. */ } |
6. Usage
To use the service is simple. Suppose that you injected the EquessiService into a property declared in your bean as the following:
1 | private EquessiService equessiService; |
And already coded its getter and setter. What you need to do is to call the method with the right parameters. – Notice here that one parameter is of List<IntelipostVolumeEntity>
type. That means that you need to build a populator/converter from your order to this type. After that, you will have the right object to pass it to the service.
Then, what you need to do is:
1 2 3 | ShipmentQuotationByVolumesResponse response = getEquessiService.getEquessiDeliveryQuotationByVolumes( origin_postal_code, origin_country_ISO, destination_postal_code, origin_country_ISO, volumes ); |
And will have the ShipmentQuotationByVolumesResponse
object in your hands to treat it as you want – show the delivery price to your customer, for instance.
7. Conclusion
In this post, I wanted to show the concept of an extension that was intended to be reused and to be replugged into various Hybris platforms without much configuration and overwrite. As shown, this extension does not do any business logic, fact that helps the extension to be decoupled.