Creating Custom Hints for beans.xml in SAP Commerce Cloud


SAP Commerce Cloud has a custom code generation mechanism for creating DTOs using declarative syntax. Declarative syntax alone makes DTOs management easy, but its modularity is the greatest thing about it. You can have multiple beans.xml configurations in separate extensions, each adding its portion of the config to the final DTO class.

In this article, we are going to explore how to utilize an undocumented hints mechanism to add custom logic to DTO generation.

In most cases, you only need to configure the list of fields and their types. However, in some cases, you may want more control over the generated class. For instance, you might need to add a custom annotation to one of the fields. Or suppose you want to automatically generate OpenAPI annotations for your WsDTOs to document APIs. There might be cases when you need even more control, like generating custom equals and hashCode methods, and so on.

There are several ways the platform allows you to customize the DTO code generation process:

  • <annotations> tag allows you to add annotations to selected DTO fields:
<property name="firstName" type="String">  
    <annotations>  
        @JsonProperty("first_name")  
    </annotations>  
</property>  
  • Using custom Velocity template. A good example is the OOTB class CommerceCartParameter. It contains a constant “DEFAULT_ENTRY_NUMBER” definition that’s defined in a custom .vm template:
<bean class="de.hybris.platform.commerceservices.service.data.CommerceCartParameter"  
          template="resources/commerce-cart-parameter-template.vm">  
  • <hints> tag. In the platform source code, it’s only used for WsDTOs to automatically generate Swagger annotations:
<bean class="de.hybris.platform.webservicescommons.dto.error.ErrorWsDTO">  
    <description>Error message</description>  
    <hints>  
        <hint name="wsRelated"/>  
        <hint name="alias">error</hint>  
    </hints>  

The last one looks like a great idea. Unfortunately, there’s no documentation for this functionality. And it’s only used in one capacity, that is, generating Swagger annotations (as of the day of this article’s publication).

Before we can add custom hints, we need to understand how the platform code processes the “<hint>” tags.

The main class orchestrating the entire process is de.hybris.bootstrap.beangenerator.BeanGenerator.

First, it parses XML configurations using de.hybris.bootstrap.beangenerator.BeansDefinitionParser class. Then, it uses de.hybris.bootstrap.beangenerator.definitions.model.PojoFactory to merge the parsed configurations into final DTOs definitions. The next step is to run the post processors. A post processor is an implementation of the de.hybris.bootstrap.beangenerator.BeansPostProcessor interface. Interestingly, there’s only one OOTB implementation of this interface: de.hybris.bootstrap.beangenerator.postprocessors.SwaggerDocumentationPostProcessor. The last step is to run the Velocity templates engine to render the final .java files.

The hints are applied during the post-processing stage. You can inspect the SwaggerDocumentationPostProcessor class yourself to find that it basically adds extra annotations to the DTOs.

Implementing Custom Hints

Now we know where we have to add our code. The question is how do we do it?

For some unknown reason, the platform only allows you to specify exactly one post processor. To do this, you need to set the generator.beans.post.processor.class property to the fully qualified class name of your post processor. This already tells us that in order to preserve the OOTB functionality we have to use composition when implementing our custom post processor.

Let’s say we want to create a hint for generating Jackson annotations to map Java camel-cased field names to JSON snake-cased fields.

We start with create a class named JsonPropertySnakeCaseBeansPostProcessor that implements the de.hybris.bootstrap.beangenerator.BeansPostProcessor interface.

First thing to add to the postProcess method’s implementation is to call the OOTB post processor. At this step, your class should look like this:

package org.training.beangenerator.postprocessors;
import de.hybris.bootstrap.beangenerator.BeansPostProcessor;
import de.hybris.bootstrap.beangenerator.definitions.model.ClassNameAware;
import de.hybris.bootstrap.beangenerator.postprocessors.SwaggerDocumentationPostProcessor;
import java.util.Collection;

public class JsonPropertySnakeCaseBeansPostProcessor implements BeansPostProcessor {
    private final SwaggerDocumentationPostProcessor swaggerDocsPostProcessor = new SwaggerDocumentationPostProcessor();
    @Override
    public Collection<? extends ClassNameAware> postProcess(Collection<? extends ClassNameAware> collection) {
        // Call OOTB post processor
        swaggerDocsPostProcessor.postProcess(collection);
        return collection;
    }
}

Now, if we assume we want to name our custom hint “snakeCaseJsonProperty”, the code that checks for the hint and adds appropriate annotations would look something like this:

// imports
public class JsonPropertySnakeCaseBeansPostProcessor implements BeansPostProcessor {
    private final SwaggerDocumentationPostProcessor swaggerDocsPostProcessor = new SwaggerDocumentationPostProcessor();
    @Override
    public Collection<? extends ClassNameAware> postProcess(Collection<? extends ClassNameAware> collection) {
        // Call OOTB post processor
        swaggerDocsPostProcessor.postProcess(collection);
        for (var bean : collection) {
            if (bean instanceof BeanPrototype beanPrototype) {
                processInternal(beanPrototype);
            }
        }
        return collection;
    }
    private void processInternal(BeanPrototype beanPrototype) {
        if (beanPrototype.getHintValue("snakeCaseJsonProperty") != null) {
            beanPrototype.addDeclaredImport("com.fasterxml.jackson.annotation.JsonProperty", false);
            for (var property : beanPrototype.getProperties()) {
                var memberAnnotations = new StringBuilder(property.getMemberAnnotations() != null ? property.getMemberAnnotations() : "");
                memberAnnotations.append("\n\t");
                memberAnnotations.append("@JsonProperty(\"").append(convertCamelToSnake(property.getName())).append("\")\n\t");
                property.setMemberAnnotations(memberAnnotations.toString());
            }
        }
    }
    private String convertCamelToSnake(String propName) {
        // code to convert camel case to snake case
    }
}

Running the code

The beans.xml used for this example looks like this:

    <bean class="org.training.beangenerator.test.TestBean">
        <hints>
            <hint name="snakeCaseJsonProperty"/>
        </hints>
        <property name="testProperty" type="String"/>
    </bean>

Now, before we compile the code we need to set the generator.beans.post.processor.class property. In my case, it looks like this in the local.properties file:

generator.beans.post.processor.class=org.training.beangenerator.postprocessors.JsonPropertySnakeCaseBeansPostProcessor

Once the property is set, compile the code normally (ant clean all). You should see the resulting DTO generating with our custom annotation added:

package org.training.beangenerator.test;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Objects;
public  class TestBean  implements Serializable 
{
/** Default serialVersionUID value. */
private static final long serialVersionUID = 1L;
/** <i>Generated property</i> for <code>TestBean.testProperty</code> property defined at extension <code>trainingcore</code>. */
@JsonProperty("test_property") 
private String testProperty;
public TestBean()
{
  // default constructor
}
public void setTestProperty(final String testProperty)
{
  this.testProperty = testProperty;
}
public String getTestProperty() 
{
  return testProperty;
}
}

Summary

Hints can be a very useful tool to customize DTO generation. Custom Velocity templates are only useful if the requirement can be implemented using declarative paradigm. If you need more dynamic logic to customize your DTOs, hints can be the solution.

 

Comments are closed, but trackbacks and pingbacks are open.