Document Properties
Kbid
31029F
Last Modified
24-Nov-2023
Added to KB
11-Oct-2023
Public Access
Everyone
Status
Online
Doc Type
Guidelines
Product
ICM 11
Guide - Add New REST Resource from Scratch via Guice

Introduction

This article describes how to create a well-coded, testable, new REST resource including a subresource for your project using Guice. It is intended to be a practical, step-by-step walkthrough that will also enhance your understanding of the topic.

Intershop recommends downloading the cartridges and using this guide in parallel to understand the respective code sections. Therefore, it is not necessary to copy the source code from each section. Use the support cartridges as a blueprint for your own business:



What Do You Learn?

Package Structure

The My Coupon REST feature is divided into the following package structure based on the CAPI (Cartridge API) approach. The main goal is to have a stable interface and class handling to build a standard package structure that is close to the defaults and make it easier to create a new REST API if needed. There are no package limitations. Feel free to change it for your own project.

Package NameDescription
com.intershop.sellside.rest.mycoupon.v1.capiHolds OpenAPI constants on versioning information. 
com.intershop.sellside.rest.mycoupon.v1.capi.handlerHolds the FeedbackHandler and encapsulates the business logic.
com.intershop.sellside.rest.mycoupon.v1.capi.request.mycouponManages and invokes the request with the given parameters. Invokes the mapper, the FeedbackHandler and the business handler.

com.intershop.sellside.rest.mycoupon.v1.capi.resourceobject.mycoupon

REST resource containing all operations for the /mycoupons path and also the item resource.

The next sections shows the CAPI and the internal implementation view which makes it easier to see the corresponding implemented logic and its purpose.

OpenAPI and Versioning

REST resources, such as an item or list resource, need a common way to retrieve general information for OpenAPI and REST versioning. The best way to do this is to encapsulate them in constant classes. This makes it more flexible when a new REST version is needed or the OpenAPI annotation changes.

The class APIConstants defines three static variables. TAG_MYCOUPON groups all REST calls under this tag and API_NAME defines the place where the new API is located. Make sure to use an existing version number for API_VERSION, in this example 1.2.0This is an important touch point for any developer who will consume and inspect the newly introduced REST API.

APIConstants
package com.intershop.sellside.rest.mycoupon.v1.capi;

public class APIConstants
{
    public static final String TAG_MYCOUPON = "Experimental";
    
    public static final String API_NAME = "promotion";
    
    public static final String API_VERSION = "1.2.0";
}

For versioning an existing REST API, the following recommendation will make it easier to handle such issues. The idea is to create a new media type and a common error message that can be used for each specific REST API.

MyCouponConstantsREST
package com.intershop.sellside.rest.mycoupon.v1.capi;

import jakarta.ws.rs.core.MediaType;

import com.intershop.sellside.rest.common.v1.capi.CommonConstantsREST.IgnoreLocalizationTest;

/**
 * Constants used by multiple REST requests/resources/resource objects.
 */
public final class MyCouponConstantsREST
{
    /**
     * A {@link String} constant representing the media type for V1 coupon requests.
     */
    @IgnoreLocalizationTest
    public static final String MEDIA_TYPE_MYCOUPON_V1_JSON = "application/vnd.intershop.mycoupon.v1+json";

    /**
     * Media type for V1 site coupon.
     */
    public static final MediaType MEDIA_TYPE_MYCOUPON_V1_JSON_TYPE = new MediaType("application", "vnd.intershop.mycoupon.v1+json");

    /**
     * Path for V1 coupon resources.
     */
    @IgnoreLocalizationTest
    public static final String RESOURCE_PATH_MY_COUPON_V1 = "mycoupons";

    /**
     * Error code if a coupon could not be resolved
     */
    public static final String ERROR_COUPON_NOT_FOUND = "mycoupon.not_found.error";

    /**
     * Error code if a coupon could not be resolved
     */
    public static final String ERROR_MISSING_EMAIL = "mycoupon.missing_email.error";
    
    /**
     * Error code if a coupon could not be resolved
     */
    public static final String ERROR_COUPON_NOT_CREATED = "mycoupon.generate_not_possible.error";

    private MyCouponConstantsREST()
    {
        // restrict instantiation
    }
}

Bring the Components Together: List Resource, Item Resource, Mapping, FeedbackHandler, REST Handler

The main idea is to use a simple class wired through Google Guice that can be replaced and handled to keep the implementation as simple as possible. Thereby the focus is on more transparency and having a testable REST API - this will be shown later.

FeedbackHandler

The following FeedbackHandler will be used later for each request to define a straight forward way to transport error messages to the client:

MyCouponFeedbackHandler
package com.intershop.sellside.rest.mycoupon.v1.capi.handler;

import jakarta.ws.rs.core.Response;

import com.intershop.sellside.rest.common.v1.capi.handler.FeedbackHandler;
import com.intershop.sellside.rest.mycoupon.v1.capi.MyCouponConstantsREST;

/**
 * Handler for REST feedback.
 */
public interface MyCouponFeedbackHandler extends FeedbackHandler
{
    /**
     * Returns a {@link Response} indicating that the requested coupon could not be found.
     * 
     * @return a {@link Response} containing an error feedback with HTTP status 404 and error code
     *         {@link MyCouponConstantsREST#ERROR_COUPON_NOT_FOUND}
     */
    Response getCouponNotFoundResponse();
    
    /**
     * Returns a {@link Response} indicating that the email is missing.
     * 
     * @return a {@link Response} containing an error feedback with HTTP status 404 and error code
     *         {@link MyCouponConstantsREST#ERROR_MISSING_EMAIL}
     */
    Response getMissingEmailResponse();
    
    /**
     * Returns a {@link Response} indicating that the coupon could not be created.
     * 
     * @return a {@link Response} containing an error feedback with HTTP status 204 and error code
     *         {@link MyCouponConstantsREST#ERROR_COUPON_NOT_CREATED}
     */
    Response getCouldNotCreateACouponResponse();
}

The exact implementation of the FeedbackHandler looks like this:

MyCouponFeedbackHandlerImpl
package com.intershop.sellside.rest.mycoupon.v1.internal.handler;

import static com.intershop.sellside.rest.mycoupon.v1.capi.MyCouponConstantsREST.ERROR_COUPON_NOT_CREATED;
import static com.intershop.sellside.rest.mycoupon.v1.capi.MyCouponConstantsREST.ERROR_COUPON_NOT_FOUND;
import static com.intershop.sellside.rest.mycoupon.v1.capi.MyCouponConstantsREST.ERROR_MISSING_EMAIL;

import jakarta.ws.rs.core.Response;

import com.intershop.sellside.rest.common.v1.capi.handler.FeedbackHandlerImpl;
import com.intershop.sellside.rest.common.v1.capi.resourceobject.feedback.FeedbackCtnrRO;
import com.intershop.sellside.rest.common.v1.capi.resourceobject.feedback.FeedbackRO;
import com.intershop.sellside.rest.mycoupon.v1.capi.handler.MyCouponFeedbackHandler;

/**
 * Default {@link MyCouponFeedbackHandler} implementation.
 */
public class MyCouponFeedbackHandlerImpl extends FeedbackHandlerImpl implements MyCouponFeedbackHandler
{
    @Override
    public Response getCouponNotFoundResponse()
    {
        FeedbackRO feedbackRO = feedbackBuilderProvider.get() //
                        .withStatus(Response.Status.NOT_FOUND) //
                        .withCode(ERROR_COUPON_NOT_FOUND) //
                        .build();
        FeedbackCtnrRO containerRO = new FeedbackCtnrRO();
        containerRO.addError(feedbackRO);
        return Response.status(Response.Status.NOT_FOUND).entity(containerRO).build();
    }

    @Override
    public Response getMissingEmailResponse()
    {
        FeedbackRO feedbackRO = feedbackBuilderProvider.get() //
                        .withStatus(Response.Status.BAD_REQUEST) //
                        .withCode(ERROR_MISSING_EMAIL) //
                        .build();
        FeedbackCtnrRO containerRO = new FeedbackCtnrRO();
        containerRO.addError(feedbackRO);
        return Response.status(Response.Status.BAD_REQUEST).entity(containerRO).build();
    }

    @Override
    public Response getCouldNotCreateACouponResponse()
    {
        FeedbackRO feedbackRO = feedbackBuilderProvider.get() //
                        .withStatus(Response.Status.NO_CONTENT) //
                        .withCode(ERROR_COUPON_NOT_CREATED) //
                        .build();
        FeedbackCtnrRO containerRO = new FeedbackCtnrRO();
        containerRO.addError(feedbackRO);
        return Response.status(Response.Status.NO_CONTENT).entity(containerRO).build();
    }

}

REST Handler

One approach that has become best practice is to encapsulate the business layer with its own interface. The business object repository (MyCouponBORepository) has its own interface, but we want to have business object requests handled by a REST API handler, such as MyCouponHandler.

We do not want to call the business layer directly, as encapsulation facilitates testing and defining the required response to the given project requirements.

MyCouponHandler
package com.intershop.sellside.rest.mycoupon.v1.capi.handler;

import java.util.Collection;

import com.intershop.support.component.mycoupon.capi.MyCouponBO;

/**
 * Handler for REST mycoupons operations.
 */
public interface MyCouponHandler
{
    /**
     * A list of {@link MyCouponBO}
     * @return A list of {@link MyCouponBO}
     */
    Collection<MyCouponBO> getMyCouponBOs();
    
    /**
     * Returns a {@link MyCouponBO} by the given couponId
     * @param couponId The coupon
     * @return The {@link MyCouponBO}
     */
    MyCouponBO getMyCouponByCode(String couponId);

    /**
     * Create a {@link MyCouponBO} by given email
     * @param email The email
     * @return The {@link MyCouponBO}
     */
    MyCouponBO generateMyCoupon(String email);         
}

The following code block shows the internal view of the MyCouponHandler:

MyCouponHandlerImpl
package com.intershop.sellside.rest.mycoupon.v1.internal.handler;

import java.util.Collection;
import java.util.Collections;

import com.google.inject.Inject;
import com.intershop.component.application.capi.ApplicationBO;
import com.intershop.component.application.capi.CurrentApplicationBOProvider;
import com.intershop.component.rest.capi.RestException;
import com.intershop.sellside.rest.mycoupon.v1.capi.handler.MyCouponHandler;
import com.intershop.support.component.mycoupon.capi.ApplicationBOMyCouponExtension;
import com.intershop.support.component.mycoupon.capi.MyCouponBO;
import com.intershop.support.component.mycoupon.capi.MyCouponBORepository;

public class MyCouponHandlerImpl implements MyCouponHandler
{
    @Inject
    private CurrentApplicationBOProvider currentApplicationBOProvider;

    @Override
    public Collection<MyCouponBO> getMyCouponBOs()
    {   
        MyCouponBORepository myCouponBORepository = getMyCouponBORepository();  
        if(myCouponBORepository==null)
        {
            return Collections.emptyList();
        }
        return myCouponBORepository.getAllCouponBOs();                        
    }

    @Override
    public MyCouponBO getMyCouponByCode(String couponId)
    {   
        MyCouponBORepository myCouponBORepository = getMyCouponBORepository();  
        if(myCouponBORepository==null)
        {
            return null;
        }   
        return myCouponBORepository.getMyCouponBOByCode(couponId);
    }
    
    @Override
    public MyCouponBO generateMyCoupon(String email)
    {
        // validate query parameter 'email'
        if (email == null || email.length() == 0)
        {
            throw new RestException().badRequest()
                            .message("Email is required to generate a coupon!")
                            .localizationKey("mycoupon.missing_email.error");
        }
        
        // get MyCouponBORepository to create a coupon
        MyCouponBORepository myCouponBORepository = getMyCouponBORepository();  
 
        // creates a coupon
        MyCouponBO myCouponBO = myCouponBORepository.createMyCouponBOByEmail(email);
        
        // validates the business object in case of error throw an exception
        if (myCouponBO == null || myCouponBO.getCode() == null)
        {
            throw new RestException().message("The Coupon code could not be generated!")
                            .localizationKey("mycoupon.generate_not_possible.error");
        }
        return myCouponBO;
    }
    
    /**
     * Returns for the given {@link ApplicationBO} the {@link MyCouponBORepository}
     * @return the {@link MyCouponBORepository}
     */
    private MyCouponBORepository getMyCouponBORepository()
    {
        ApplicationBOMyCouponExtension applicationBOMyCouponExtension = currentApplicationBOProvider.get()
                        .getExtension(ApplicationBOMyCouponExtension.EXTENSION_ID);
        return applicationBOMyCouponExtension.getMyCouponBORepository();
    }
}

Handle - MyCouponItemGetRequest

The following class demonstrates the item request handling, for example for a call like: https://localhost:8443/INTERSHOP/rest/WFS/inSPIRED-inTRONICS-Site/-/mycoupons/25bc253b-db4f-4186-a6c0-8dd6d2bb9805

The MyCouponItemGetRequest class is responsible for interacting with the business layer to get a MyCouponBO for a specific coupon and mapping that information to a MyCouponRO resource object. Note that the implementation also addresses error handling, which is handled by the introduced FeedbackHandler.

This simple example tries to get an exact MyCouponRO for a coupon. If it is not found, it generates a standardized error and sends it to the client. This example shows the strength of the predefined classes approach, especially as the complexity of a REST API increases

MyCouponItemGetRequest
package com.intershop.sellside.rest.mycoupon.v1.capi.request.mycoupon;

import java.util.function.Function;

import javax.inject.Inject;
import javax.inject.Provider;

import com.intershop.component.rest.capi.resource.RestResourceCacheHandler;
import com.intershop.sellside.rest.common.v1.capi.resourceobject.common.ContainerRO;
import com.intershop.sellside.rest.common.v1.capi.resourceobject.common.ContainerROBuilder;
import com.intershop.sellside.rest.mycoupon.v1.capi.handler.MyCouponFeedbackHandler;
import com.intershop.sellside.rest.mycoupon.v1.capi.handler.MyCouponHandler;
import com.intershop.sellside.rest.mycoupon.v1.capi.resourceobject.mycoupon.MyCouponRO;
import com.intershop.support.component.mycoupon.capi.MyCouponBO;

import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo;

/**
 * Request for "GET /mycoupons/<coupon-id>".
 */
public class MyCouponItemGetRequest
{
    private MyCouponHandler myCouponHandler;
    private RestResourceCacheHandler cacheHandler;
    private MyCouponFeedbackHandler myCouponFeedbackHandler;
    private Function<MyCouponBO, MyCouponRO> myCouponROMapper;
    private Provider<ContainerROBuilder<MyCouponRO>> containerROBuilderProvider;

    @Inject
    public MyCouponItemGetRequest(MyCouponHandler myCouponHandler, RestResourceCacheHandler cacheHandler,
                    MyCouponFeedbackHandler myCouponFeedbackHandler, Function<MyCouponBO, MyCouponRO> myCouponROMapper,
                    Provider<ContainerROBuilder<MyCouponRO>> containerROBuilderProvider)
    {
        this.myCouponHandler = myCouponHandler;
        this.cacheHandler = cacheHandler;
        this.myCouponFeedbackHandler = myCouponFeedbackHandler;
        this.myCouponROMapper = myCouponROMapper;
        this.containerROBuilderProvider = containerROBuilderProvider;
    }

    /**
     * Invokes the request with the given parameters.
     * 
     * @param uriInfo
     *            the URI information for the current request
     * @param couponCode
     *            the coupon code of to return details for
     * @return the response
     */
    public Response invoke(UriInfo uriInfo, String couponCode)
    {
        cacheHandler.setCacheExpires(0);

        MyCouponBO myCouponBO = myCouponHandler.getMyCouponByCode(couponCode);    
        
        if (myCouponBO == null)
        {
            return myCouponFeedbackHandler.getCouponNotFoundResponse();
        }

        MyCouponRO myCouponRO = myCouponROMapper.apply(myCouponBO);

        ContainerRO<MyCouponRO> containerRO = containerROBuilderProvider.get() //
                        .withData(myCouponRO) //
                        .withUriInfo(uriInfo) //
                        .build();

        return Response.ok(containerRO).build();
    }
}

Handle - MyCouponListGetRequest

Now we can introduce a way to get a list of MyCouponROs, similar to an item request:

MyCouponListGetRequest
package com.intershop.sellside.rest.mycoupon.v1.capi.request.mycoupon;

import java.util.Collection;
import java.util.function.Function;

import javax.inject.Inject;
import javax.inject.Provider;

import com.intershop.component.rest.capi.resource.RestResourceCacheHandler;
import com.intershop.sellside.rest.common.v1.capi.resourceobject.common.ContainerRO;
import com.intershop.sellside.rest.common.v1.capi.resourceobject.common.ContainerROBuilder;
import com.intershop.sellside.rest.mycoupon.v1.capi.handler.MyCouponHandler;
import com.intershop.sellside.rest.mycoupon.v1.capi.resourceobject.mycoupon.MyCouponRO;
import com.intershop.support.component.mycoupon.capi.MyCouponBO;

import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo;

/**
 * Request for "GET /mycoupons".
 */
public class MyCouponListGetRequest
{
    private MyCouponHandler myCouponHandler;
    private RestResourceCacheHandler cacheHandler;
    private Function<Collection<MyCouponBO>, Collection<MyCouponRO>> myCouponROMapper;
    private Provider<ContainerROBuilder<Collection<MyCouponRO>>> containerROBuilderProvider;

    @Inject
    public MyCouponListGetRequest(MyCouponHandler myCouponHandler, RestResourceCacheHandler cacheHandler,
                    Function<Collection<MyCouponBO>, Collection<MyCouponRO>> versionROMapper,
                    Provider<ContainerROBuilder<Collection<MyCouponRO>>> containerROBuilderProvider)
    {
        this.myCouponHandler = myCouponHandler;
        this.cacheHandler = cacheHandler;
        this.myCouponROMapper = versionROMapper;
        this.containerROBuilderProvider = containerROBuilderProvider;
    }

    /**
     * Invokes the request with the given parameters.
     * 
     * @param uriInfo
     *            the URI information for the current request
     * @return the response
     */
    public Response invoke(UriInfo uriInfo)
    {
        cacheHandler.setCacheExpires(0);
        
        Collection<MyCouponBO> myCouponBOs = myCouponHandler.getMyCouponBOs();       
        Collection<MyCouponRO> myCouponROs = myCouponROMapper.apply(myCouponBOs);
        
        ContainerRO<Collection<MyCouponRO>> containerRO = containerROBuilderProvider.get() //
                        .withData(myCouponROs) //
                        .withUriInfo(uriInfo) //
                        .build();
        
        return Response.ok(containerRO).build();
    }
}

Handle - MyCouponListPostRequest

Finally, we want a class that allows us to create a new coupon via a POST request:

MyCouponListPostRequest
package com.intershop.sellside.rest.mycoupon.v1.capi.request.mycoupon;

import java.util.function.Function;

import javax.inject.Inject;
import javax.inject.Provider;

import com.intershop.component.rest.capi.resource.RestResourceCacheHandler;
import com.intershop.sellside.rest.common.v1.capi.resourceobject.common.ContainerRO;
import com.intershop.sellside.rest.common.v1.capi.resourceobject.common.ContainerROBuilder;
import com.intershop.sellside.rest.mycoupon.v1.capi.handler.MyCouponFeedbackHandler;
import com.intershop.sellside.rest.mycoupon.v1.capi.handler.MyCouponHandler;
import com.intershop.sellside.rest.mycoupon.v1.capi.resourceobject.mycoupon.MyCouponRO;
import com.intershop.support.component.mycoupon.capi.MyCouponBO;

import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo;

/**
 * Request for "POST /mycoupons".
 */
public class MyCouponListPostRequest
{
    private MyCouponHandler myCouponHandler;
    private RestResourceCacheHandler cacheHandler;
    private MyCouponFeedbackHandler myCouponFeedbackHandler;
    private Function<MyCouponBO, MyCouponRO> myCouponROMapper;
    private Provider<ContainerROBuilder<MyCouponRO>> containerROBuilderProvider;

    @Inject
    public MyCouponListPostRequest(MyCouponHandler myCouponHandler, RestResourceCacheHandler cacheHandler,
                    MyCouponFeedbackHandler myCouponFeedbackHandler, Function<MyCouponBO, MyCouponRO> myCouponROMapper,
                    Provider<ContainerROBuilder<MyCouponRO>> containerROBuilderProvider)
    {
        this.myCouponHandler = myCouponHandler;
        this.cacheHandler = cacheHandler;
        this.myCouponFeedbackHandler = myCouponFeedbackHandler;
        this.myCouponROMapper = myCouponROMapper;
        this.containerROBuilderProvider = containerROBuilderProvider;
    }

    /**
     * Invokes the request with the given parameters.
     * 
     * @param uriInfo
     *            the URI information for the current request
     * @param email 
     * @return the response
     */
    public Response invoke(UriInfo uriInfo, String email)
    {
        cacheHandler.setCacheExpires(0);
        
        MyCouponBO myCouponBO = myCouponHandler.generateMyCoupon(email);        

        if(myCouponBO == null)
        {
            myCouponFeedbackHandler.getCouldNotCreateACouponResponse();
        }
        
        MyCouponRO myCouponRO = myCouponROMapper.apply(myCouponBO);
        
        ContainerRO<MyCouponRO> containerRO = containerROBuilderProvider.get() //
                        .withData(myCouponRO) //
                        .withUriInfo(uriInfo) //
                        .build();
        
        return Response.ok(containerRO).build();
    }
}

REST Resources

The following two operations show the root resource and the corresponding sub resource to fulfill the following requirement:

Item Resource - MyCouponItemResource

The following REST request matches to the current sub resource:

This class has the following advantages:

  • No business logic
  • Easy JUnit testing
  • OpenAPI annotated
  • Easy versioning through MyCouponConstantsREST class
  • Easy to read and understand

These advantages also apply to all other REST artifacts.

MyCouponItemResource
package com.intershop.sellside.rest.mycoupon.v1.capi.resource.mycoupon;

import static com.intershop.sellside.rest.mycoupon.v1.capi.MyCouponConstantsREST.MEDIA_TYPE_MYCOUPON_V1_JSON;

import com.google.inject.Inject;
import com.intershop.sellside.rest.common.v1.capi.resourceobject.feedback.FeedbackCtnrRO;
import com.intershop.sellside.rest.mycoupon.v1.capi.APIConstants;
import com.intershop.sellside.rest.mycoupon.v1.capi.request.mycoupon.MyCouponItemGetRequest;
import com.intershop.sellside.rest.mycoupon.v1.capi.resourceobject.mycoupon.MyCouponRO;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.container.ResourceContext;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo;

/**
 * REST resource containing all operations for path "/mycoupons/<coupon-id>".
 */
@Tag(name = APIConstants.TAG_MYCOUPON)
public class MyCouponItemResource
{
    @Inject
    private MyCouponItemGetRequest myCouponGetRequest;
    @Context
    private UriInfo uriInfo;
    @Context
    private ResourceContext context;

    private String couponId;

    public MyCouponItemResource(String couponId)
    {
        this.couponId = couponId;
    }

    @Operation(summary = "Returns the coupons details for given coupon code.",
            description = "Returns the coupon details for given coupon code. If coupon with this code is not found "
                    + "a 404 error will be returned.")
    @ApiResponse(responseCode = "200", description = "The requested coupon details.",
            content = @Content(schema = @Schema(implementation = MyCouponRO.class)))
    @ApiResponse(responseCode = "404", description = "If a coupon with the given code is not found.",
            content = @Content(schema = @Schema(implementation = FeedbackCtnrRO.class)))
    @GET
    @Produces({MEDIA_TYPE_MYCOUPON_V1_JSON})
    public Response getMyCoupon_V1()
    {
        return myCouponGetRequest.invoke(uriInfo, couponId);
    }
}

List Resource - MyCouponListResource

The previous resource is a sub resource of the root resource MyCouponListResource. Note that the root resource is the main entry point.

Particularly noteworthy in this implementation are the following aspects:

  • Root resource and main interaction point for REST handling
  • A wired sub resource MyCouponItemResource
  • Defined POST and GET requests

Another important aspect is versioning, which is represented by the method signature, making it easier to distinguish between different REST APIs.

MyCouponListResource
package com.intershop.sellside.rest.mycoupon.v1.capi.resource.mycoupon;

import static com.intershop.sellside.rest.mycoupon.v1.capi.APIConstants.API_NAME;
import static com.intershop.sellside.rest.mycoupon.v1.capi.APIConstants.API_VERSION;
import static com.intershop.sellside.rest.mycoupon.v1.capi.APIConstants.TAG_MYCOUPON;
import static com.intershop.sellside.rest.mycoupon.v1.capi.MyCouponConstantsREST.MEDIA_TYPE_MYCOUPON_V1_JSON;
import static com.intershop.sellside.rest.mycoupon.v1.capi.MyCouponConstantsREST.RESOURCE_PATH_MY_COUPON_V1;

import com.google.inject.Inject;
import com.google.inject.Injector;
import com.intershop.component.rest.capi.openapi.OpenAPIConstants;
import com.intershop.component.rest.capi.transaction.Transactional;
import com.intershop.sellside.rest.mycoupon.v1.capi.request.mycoupon.MyCouponListGetRequest;
import com.intershop.sellside.rest.mycoupon.v1.capi.request.mycoupon.MyCouponListPostRequest;
import com.intershop.sellside.rest.mycoupon.v1.capi.resourceobject.mycoupon.MyCouponRO;

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.container.ResourceContext;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo;

/**
 * REST resource containing all operations for path "/mycoupons".
 */
@Tag(name = TAG_MYCOUPON)
@OpenAPIDefinition(info = @Info(version = API_VERSION), extensions = @Extension(
        properties = {@ExtensionProperty(name = OpenAPIConstants.API_ID, value = API_NAME)}))
@Path(RESOURCE_PATH_MY_COUPON_V1)
public class MyCouponListResource
{
    @Inject 
    private Injector injector;
    @Inject
    private MyCouponListGetRequest myCouponListGetRequest;
    @Inject
    private MyCouponListPostRequest myCouponListPostRequest;
    @Context
    private UriInfo uriInfo;
    @Context    
    private ResourceContext context;

    @Operation(summary = "Returns the available coupons.",
            description = "Returns the available coupons.")
    @ApiResponse(responseCode = "200", description = "The list of available coupons.",
            content = @Content(schema = @Schema(implementation = MyCouponRO.class)))
    @GET
    @Produces({MEDIA_TYPE_MYCOUPON_V1_JSON})
    public Response getAllMyCoupons_V1()
    {
        return myCouponListGetRequest.invoke(uriInfo);
    }
       
    /**
     * Returns for a given email a coupon.
     */
    @Operation(summary = "Creates a coupon.")
    @POST
    @Produces({MEDIA_TYPE_MYCOUPON_V1_JSON})
    @Transactional
    public Response generateMyCoupon_V1(@QueryParam("email") String email) 
    {
        return myCouponListPostRequest.invoke(uriInfo, email);
    }

    @Path("{couponId}")
    public MyCouponItemResource getMyCouponItemResource(@PathParam("couponId") String couponId)
    {
        MyCouponItemResource resource = new MyCouponItemResource(couponId);
        context.initResource(resource);
        injector.injectMembers(resource);
        return resource;
    }
}

Resource Object and Mapper

Every new REST API needs a resource object. For this use case and corresponding to the MyCouponBO, the MyCouponRO is introduced, which shows a small part of the underlying MyCouponPO and MyCouponBO.

MyCouponRO
package com.intershop.sellside.rest.mycoupon.v1.capi.resourceobject.mycoupon;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.intershop.component.rest.capi.resourceobject.AbstractResourceObject;
import com.intershop.support.component.mycoupon.capi.MyCouponBO;

import io.swagger.v3.oas.annotations.media.Schema;

/**
 * This is the resource object for my coupon code.
 */
@Schema(name = "MyCouponRO_v1", description = "Describes a Coupon object.")
@JsonInclude(value = JsonInclude.Include.NON_EMPTY)
@JsonPropertyOrder(alphabetic = true)
public class MyCouponRO extends AbstractResourceObject
{
    public final static String TYPENAME = "MyCoupon";
    public final static String NAME = "MyCoupon";

    private String code;

    public MyCouponRO()
    {
        super(NAME);
    }

    /**
     * @return the coupon code
     */
    @Schema(description = "The coupon code", example = "US", accessMode = Schema.AccessMode.READ_ONLY, type = "java.lang.String")
    public String getCode()
    {
        return code;
    }

    /**
     * @param code the coupon code to set
     */
    public void setCode(String code)
    {
        this.code = code;
    }

    @Override
    @Schema(description = "The type of the object", example = TYPENAME)
    public String getType()
    {
        return TYPENAME;
    }   
    
}

The following section shows the resource object mapper for a given business object MyCouponBO. This mapper can be injected and can also use other ICM code artifacts to enrich a resource object if needed.

MyCouponROMapper
package com.intershop.sellside.rest.mycoupon.v1.internal.mapper.mycoupon;

import java.util.function.Function;

import com.intershop.sellside.rest.mycoupon.v1.capi.resourceobject.mycoupon.MyCouponRO;
import com.intershop.support.component.mycoupon.capi.MyCouponBO;

public class MyCouponROMapper implements Function<MyCouponBO, MyCouponRO>
{
    @Override
    public MyCouponRO apply(MyCouponBO myCouponBO)
    {
        MyCouponRO myCouponRO = new MyCouponRO();
        myCouponRO.setCode(myCouponBO.getCode());
        return myCouponRO;
    }
}

REST API Guice Wiring

Another important aspect is the Google Guice wiring for this new REST API. The following modules are created to make this example work. Normally you can put all the wiring into a single module, but it makes sense to avoid this in favor of more flexibility and more transparency.

Module - AppSfRestMyCouponBuilderModule

AppSfRestMyCouponBuilderModule
package com.intershop.sellside.rest.mycoupon.v1.internal.modules;

import java.util.Collection;

import com.google.inject.AbstractModule;
import com.google.inject.TypeLiteral;
import com.intershop.sellside.rest.common.v1.capi.resourceobject.common.ContainerROBuilder;
import com.intershop.sellside.rest.mycoupon.v1.capi.resourceobject.mycoupon.MyCouponRO;

public class AppSfRestMyCouponBuilderModule extends AbstractModule
{
    @Override
    protected void configure()
    {
        bind(new TypeLiteral<ContainerROBuilder<Collection<MyCouponRO>>>() {});
    }
}

Module - AppSfRestMyCouponHandlerModule

AppSfRestMyCouponHandlerModule
package com.intershop.sellside.rest.mycoupon.v1.internal.modules;

import com.google.inject.AbstractModule;
import com.intershop.sellside.rest.mycoupon.v1.capi.handler.MyCouponFeedbackHandler;
import com.intershop.sellside.rest.mycoupon.v1.capi.handler.MyCouponHandler;
import com.intershop.sellside.rest.mycoupon.v1.internal.handler.MyCouponFeedbackHandlerImpl;
import com.intershop.sellside.rest.mycoupon.v1.internal.handler.MyCouponHandlerImpl;

public class AppSfRestMyCouponHandlerModule extends AbstractModule
{
    @Override
    protected void configure()
    {
        bind(MyCouponFeedbackHandler.class).to(MyCouponFeedbackHandlerImpl.class);
        bind(MyCouponHandler.class).to(MyCouponHandlerImpl.class);
    }
}

Module - AppSfRestMyCouponMapperModule

AppSfRestMyCouponMapperModule
package com.intershop.sellside.rest.mycoupon.v1.internal.modules;

import java.util.Collection;
import java.util.function.Function;

import com.google.inject.AbstractModule;
import com.google.inject.TypeLiteral;
import com.intershop.sellside.rest.common.v1.capi.mapper.CollectionFunction;
import com.intershop.sellside.rest.mycoupon.v1.capi.resourceobject.mycoupon.MyCouponRO;
import com.intershop.sellside.rest.mycoupon.v1.internal.mapper.mycoupon.MyCouponROMapper;
import com.intershop.support.component.mycoupon.capi.MyCouponBO;

public class AppSfRestMyCouponMapperModule extends AbstractModule
{
    @Override
    protected void configure()
    {
        // common
        bind(new TypeLiteral<Function<MyCouponBO, MyCouponRO>>() {}).to(MyCouponROMapper.class);
        bind(new TypeLiteral<Function<Collection<MyCouponBO>, Collection<MyCouponRO>>>() {})
            .to(new TypeLiteral<CollectionFunction<MyCouponBO, MyCouponRO>>() {});
    }
}

Module - AppSfRestMyCouponRequestModule 

AppSfRestMyCouponRequestModule
package com.intershop.sellside.rest.mycoupon.v1.internal.modules;

import com.google.inject.AbstractModule;
import com.intershop.sellside.rest.mycoupon.v1.capi.request.mycoupon.MyCouponItemGetRequest;
import com.intershop.sellside.rest.mycoupon.v1.capi.request.mycoupon.MyCouponListGetRequest;
import com.intershop.sellside.rest.mycoupon.v1.capi.request.mycoupon.MyCouponListPostRequest;

public class AppSfRestMyCouponRequestModule extends AbstractModule
{
    @Override
    protected void configure()
    {
        /*
         * MyCoupon resource
         */
        bind(MyCouponListGetRequest.class);
        bind(MyCouponItemGetRequest.class);
        bind(MyCouponListPostRequest.class);
    }
}

Module - AppSfRestMyCouponResourceModule 

AppSfRestMyCouponResourceModule
package com.intershop.sellside.rest.mycoupon.v1.internal.modules;

import com.google.inject.AbstractModule;
import com.google.inject.multibindings.Multibinder;
import com.intershop.component.rest.capi.resource.SubResourceProvider;
import com.intershop.sellside.rest.mycoupon.v1.internal.resource.MyCouponListResourceProvider;

public class AppSfRestMyCouponResourceModule extends AbstractModule
{
    @Override
    protected void configure()
    {
        Multibinder<SubResourceProvider> subResourcesBinder = Multibinder.newSetBinder(binder(),
                        SubResourceProvider.class);
        subResourcesBinder.addBinding().to(MyCouponListResourceProvider.class);
    }
}

Module - MyCouponListResourceProvider

The following provider is of interest because it shows the ACL handling and wiring of the MyCouponListResource class:

MyCouponListResourceProvider
package com.intershop.sellside.rest.mycoupon.v1.internal.resource;

import javax.ws.rs.container.ResourceContext;

import com.intershop.component.rest.capi.resource.RestResource;
import com.intershop.component.rest.capi.resource.RootResource;
import com.intershop.component.rest.capi.resource.SubResourceProvider;
import com.intershop.sellside.rest.mycoupon.v1.capi.MyCouponConstantsREST;
import com.intershop.sellside.rest.mycoupon.v1.capi.resource.mycoupon.MyCouponListResource;

/**
 * {@link SubResourceProvider} for {@link MyCouponListResource}.
 */
public class MyCouponListResourceProvider implements SubResourceProvider
{
    public static final String MY_COUPON_RESOURCE_ACL_PATH = "resources/support_app_sf_rest_mycoupon/rest/mycoupon-acl.properties";

    @Override
    public Object getSubResource(RestResource parent, ResourceContext rc, String subResourceName)
    {
        if (isApplicable("support_app_sf_rest_mycoupon") && MyCouponConstantsREST.RESOURCE_PATH_MY_COUPON_V1.equals(subResourceName))
        {
            parent.getRootResource().addACLRuleIfSupported(MY_COUPON_RESOURCE_ACL_PATH);
            Object resource = getSubResource(parent);
            rc.initResource(resource);
            return resource;
        }
        return null;
    }

    @Override
    public Object getSubResource(RestResource parent)
    {
        // this method is only used for generating the API model, so no need to check the media type and path here
        if (parent instanceof RootResource && isApplicable("support_app_sf_rest_mycoupon"))
        {
            return new MyCouponListResource();
        }
        return null;
    }
}

ACL and Module Location

All of these modules are defined in objectgraph.properties and ACL permission handling is defined in mycoupon-acl.properties.


objectgraph.properties
global.modules = com.intershop.sellside.rest.mycoupon.v1.internal.modules.AppSfRestMyCouponBuilderModule \
                 com.intershop.sellside.rest.mycoupon.v1.internal.modules.AppSfRestMyCouponHandlerModule \
                 com.intershop.sellside.rest.mycoupon.v1.internal.modules.AppSfRestMyCouponMapperModule \
                 com.intershop.sellside.rest.mycoupon.v1.internal.modules.AppSfRestMyCouponRequestModule \
                 com.intershop.sellside.rest.mycoupon.v1.internal.modules.AppSfRestMyCouponResourceModule
mycoupon-acl.properties
# ACL Entries for mycoupon REST Calls
GET|mycoupons{anyPath\:.*}=isAnyUser
POST|mycoupons{anyPath\:.*}=isAnyUser

Localization

The previous sections included localization keys such as mycoupon.not_found.error for the FeedbackHandler. All these keys are localized:


mycoupon_en.properties
mycoupon.not_found.error=Coupon could not be found
mycoupon.missing_email.error=Email required
mycoupon.generate_not_possible.error=Could not create a coupon

Enable the New Cartridge for REST

To enable the new cartridge for the REST application:

  1. Create a file apps.component:

    apps.component
    <?xml version="1.0" encoding="UTF-8"?>
    <?xml version="1.0" encoding="UTF-8"?>
    <components xmlns="http://www.intershop.de/component/2010">
    	<fulfill requirement="selectedCartridge" of="intershop.REST.Cartridges" value="support_app_sf_rest_mycoupon" />
    </components>
  2. Register the new cartridge in ft_production.

    build.gradle
    plugins {
        id 'java'
        id 'com.intershop.icm.cartridge.product'
    }
    
    description = 'Runnable Feature - all headless cartridges'
    
    dependencies {
    
        cartridgeRuntime "com.intershop.icm:ft_icm_as"
    
        cartridgeRuntime "com.intershop.solrcloud:ac_solr_cloud"
        cartridgeRuntime "com.intershop.solrcloud:ac_solr_cloud_bo"
    
        // pwa
        cartridgeRuntime "com.intershop.headless:app_sf_pwa_cm"
        
        // add your production cartridge here
        cartridgeRuntime project(":my_cartridge")
        cartridgeRuntime project(":support_app_sf_rest_mycoupon")
    }

JUnit Tests for the New REST API

Intershop recommends creating JUnit tests for key features of a project, as they become increasingly important for ICM portability between versions and customer success. The demo cartridge does not provide a complete set of tests, but it provides some examples that you can use as a basis for creating your own tests.

Note that there is no obligation to create JUnit tests. However, they help to gain more clarity about your REST API, especially regarding the functionality covered and whether a version is stable. 

The JUnit tests can be found in the following location:

Create a New Organization

  1. Start the MSSQL database in the project folder using the following command: gradlew startMSSQL
    If no MSSQL database has been downloaded, the pullMSSQL command is executed.
  2. Prepare the database by using: gradlew dbPrepare 
  3. Start the WebAdapter, WebAdapteragent and the Server with the following command: gradlew startServer
  4. Open https://localhost:8443/INTERSHOP/web/WFS/SLDSystem/- and log in to Organization Management (via Operations).
  5. Create a new organization inSPIRED:
  6. Log in to the new organization inSPIRED:
  7. Create a new channel, for example inTRONICS:

    The new REST cartridge is wired to the intershop.REST application.

Explore the New REST API 

SMC

To find the new REST API in the SMC, perform the following steps:

  1. Open the SMC and switch to Site Management | <your site | Applications | General and click Open Swagger UI.

    This might take a few seconds. 
  2. Click Promotion 1.2.0 to open the new REST API. 
  3. Scroll down to the Experimental section, where you can find the newly introduced REST calls. 

Postman

Postman is a powerful tool to test a REST API or to create an automated REST test scenario. Use the following Postman collection to interact with the new REST API.

Postman collection: MyCoupon API.postman_collection.json

Disclaimer
The information provided in the Knowledge Base may not be applicable to all systems and situations. Intershop Communications will not be liable to any party for any direct or indirect damages resulting from the use of the Customer Support section of the Intershop Corporate Web site, including, without limitation, any lost profits, business interruption, loss of programs or other data on your information handling system.
The Intershop Knowledge Portal uses only technically necessary cookies. We do not track visitors or have visitors tracked by 3rd parties. Please find further information on privacy in the Intershop Privacy Policy and Legal Notice.
Home
Knowledge Base
Product Releases
Log on to continue
This Knowledge Base document is reserved for registered customers.
Log on with your Intershop Entra ID to continue.
Write an email to supportadmin@intershop.de if you experience login issues,
or if you want to register as customer.