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:
support_bc_mycoupon
support_app_sf_rest_mycoupon
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 Name | Description |
---|---|
com.intershop.sellside.rest.mycoupon.v1.capi | Holds OpenAPI constants on versioning information. |
com.intershop.sellside.rest.mycoupon.v1.capi.handler | Holds the FeedbackHandler and encapsulates the business logic. |
com.intershop.sellside.rest.mycoupon.v1.capi.request.mycoupon | Manages 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.
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.0
. This is an important touch point for any developer who will consume and inspect the newly introduced REST API.
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.
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 } }
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.
The following FeedbackHandler will be used later for each request to define a straight forward way to transport error messages to the client:
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:
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(); } }
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.
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
:
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(); } }
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
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(); } }
Now we can introduce a way to get a list of MyCouponRO
s, similar to an item request:
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(); } }
Finally, we want a class that allows us to create a new coupon via a POST request:
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(); } }
The following two operations show the root resource and the corresponding sub resource to fulfill the following requirement:
The following REST request matches to the current sub resource:
This class has the following advantages:
MyCouponConstantsREST
classThese advantages also apply to all other REST artifacts.
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); } }
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:
Another important aspect is versioning, which is represented by the method signature, making it easier to distinguish between different REST APIs.
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; } }
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
.
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.
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; } }
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.
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>>>() {}); } }
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); } }
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>>() {}); } }
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); } }
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); } }
The following provider is of interest because it shows the ACL handling and wiring of the MyCouponListResource
class:
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; } }
All of these modules are defined in objectgraph.properties and ACL permission handling is defined in mycoupon-acl.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
# ACL Entries for mycoupon REST Calls GET|mycoupons{anyPath\:.*}=isAnyUser POST|mycoupons{anyPath\:.*}=isAnyUser
The previous sections included localization keys such as mycoupon.not_found.error for the FeedbackHandler. All these keys are localized:
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
To enable the new cartridge for the REST application:
Create a file 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>
Register the new cartridge in ft_production.
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") }
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:
gradlew startMSSQL
pullMSSQL
command is executed.gradlew dbPrepare
gradlew startServer
To find the new REST API in the SMC, perform the following steps:
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