This article describes how to add a new REST resource to an existing REST API. It is intended as a practical walkthrough which will provide information and understanding to start creating a new REST resource. Also included is the creation of a new persistent object in combination with an own business object.
It is not necessary to copy the source code from each section. Use the support cartridges for your own business as a blueprint:
support_bc_mycoupon
support_app_sf_rest_mycoupon
This chapter explains how to create a new support_by_mycoupon business cartridge via Intershop Studio. This cartridge contains a new persistent object MyCouponPO and also the new business object MyCouponBO.
import "enfinity:/core/edl/com/intershop/beehive/core/types.edl"; external PersistentObject type "com.intershop.beehive.core.capi.domain.PersistentObject"; namespace com.intershop.support.component.mycoupon.internal { cartridge interface MyCoupon extends PersistentObject { /* * The email for mycoupon */ attribute email: string required; /* * The coupon text */ attribute coupon: string required ; /** * The creation date */ attribute creationDate : datetime required; } }
import "enfinity:/support_bc_mycoupon/edl/com/intershop/support/component/mycoupon/capi/MyCoupon.edl"; import "enfinity:/core/edl/com/intershop/beehive/core/capi/domain/PersistentObjectPO.edl"; import "enfinity:/core/edl/com/intershop/beehive/core/types.edl"; namespace com.intershop.support.component.mycoupon.internal { orm class MyCouponPO extends PersistentObjectPO implements MyCoupon { /* * The email for mycoupon */ attribute email: string required; /* * The coupon text for mycoupon */ attribute coupon: string<256> required ; /** * The creation date */ attribute creationDate : datetime required; index(email, coupon); } }
Open the context menu and click Generate Code (EDL-Models).
This generates the classes based on your new EDL files.
In case of success the console output contains the following lines:
Generate code for cartride support_bc_mycoupon Generated but NOT changed: '/support_bc_mycoupon/src/main/resources/com/intershop/support/component/mycoupon/internal/MyCouponPO.dbconstraints.oracle.ddl' Generated but NOT changed: '/support_bc_mycoupon/src/main/resources/com/intershop/support/component/mycoupon/internal/MyCouponPO.dbconstraints.microsoft.ddl' Generated but NOT changed: '/support_bc_mycoupon/src/main/resources/com/intershop/support/component/mycoupon/internal/MyCouponPO.dbindex.oracle.ddl' Generated but NOT changed: '/support_bc_mycoupon/src/main/resources/com/intershop/support/component/mycoupon/internal/MyCouponPO.dbindex.microsoft.ddl' Generated but NOT changed: '/support_bc_mycoupon/src/main/java/com/intershop/support/component/mycoupon/internal/MyCouponPOKey.java' Generated but NOT changed: '/support_bc_mycoupon/src/main/java/com/intershop/support/component/mycoupon/internal/MyCouponPO.java' Generated but NOT changed: '/support_bc_mycoupon/src/main/java/com/intershop/support/component/mycoupon/internal/MyCouponPOFactory.java' Generated but NOT changed: '/support_bc_mycoupon/src/main/resources/com/intershop/support/component/mycoupon/internal/MyCouponPO.orm' Generated but NOT changed: '/support_bc_mycoupon/src/main/java/com/intershop/support/component/mycoupon/internal/MyCoupon.java'
With the newly created MyCouponPO we can create our own business object. The goal is to use it in our new REST API and to be able to invoke the extension through ApplicationBO. Additionally, we want to use our new business object via ISML and pipelines.
To create a new business project, perform the following steps:
Create a new interface MyCouponBO.
In this case we keep it simple and our class contains only two methods. Feel free to create more methods for your business layer to hide internal functionality. For more information about business objects, refer to Concept - Business Objects.
package com.intershop.support.component.mycoupon.capi; import com.intershop.beehive.businessobject.capi.BusinessObject; /** * A simple business object with only two attributes. */ public interface MyCouponBO extends BusinessObject { /** * @return code of the coupon */ String getCode(); /** * * @return email of the coupon */ String getEmail(); }
package com.intershop.support.component.mycoupon.internal; import com.intershop.beehive.businessobject.capi.BusinessObjectContext; import com.intershop.beehive.core.capi.domain.AbstractPersistentObjectBO; import com.intershop.support.component.mycoupon.capi.MyCouponBO; public class MyCouponBOImpl extends AbstractPersistentObjectBO<MyCouponPO> implements MyCouponBO { public MyCouponBOImpl(MyCouponPO myCouponPO, BusinessObjectContext context) { super(myCouponPO, context); } @Override public String getCode() { return this.getPersistentObject() .getCoupon(); } @Override public String getEmail() { return this.getPersistentObject() .getEmail(); } }
Our business object is ready.
Create a repository, therefore create a new interface MyCouponBORepository which manages the life cycle:
package com.intershop.support.component.mycoupon.capi; import com.intershop.beehive.businessobject.capi.BusinessObjectRepository; /** * Repository for {@link MyCouponBO}. */ public interface MyCouponBORepository extends BusinessObjectRepository { public static final String EXTENSION_ID = "MyCouponBORepository"; /** * Creates a coupon by the given email if the email has already an active coupon, * the active coupon will be returned. * * @param email - the email for the coupon * * @return a new {@link MyCouponBO} for the given email **/ MyCouponBO createMyCouponBOByEmail(String email); /** * Counts all {@link MyCouponBO}s for the given email. * * @return the count of all {@link MyCouponBO} for a single email **/ int countMyCouponsByEmail(String email); /** * Gets a {@link MyCouponBO} by a coupon code * * @param couponCode - the coupon code * @return the {@link MyCouponBO} */ MyCouponBO getMyCouponBOByCode(String couponCode); }
package com.intershop.support.component.mycoupon.internal; import java.nio.charset.Charset; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Random; import com.google.inject.Inject; import com.intershop.beehive.core.capi.util.ObjectMapper; import com.intershop.beehive.orm.capi.common.ORMObjectCollection; import com.intershop.component.repository.capi.AbstractDomainRepositoryBOExtension; import com.intershop.component.repository.capi.RepositoryBO; import com.intershop.support.component.mycoupon.capi.MyCouponBO; import com.intershop.support.component.mycoupon.capi.MyCouponBORepository; public class MyCouponBORepositoryImpl extends AbstractDomainRepositoryBOExtension implements MyCouponBORepository, ObjectMapper<MyCouponPO, MyCouponBO> { /** * Stores all MyCoupons to it's persistance objects */ private Map<MyCouponPO, MyCouponBO> cache = new HashMap<>(); @Inject private MyCouponPOFactory myCouponPOFactory; public MyCouponBORepositoryImpl(String extensionID, RepositoryBO extendedObject) { super(extensionID, extendedObject); } /** * Returns the {@link MyCouponBO} for it's persistance object. If it's not existing, a new my coupon is created. * * @param myCouponPO * - persistance object, that should converted to its business object * @return the {@link MyCouponBO} for the given persistance object */ @Override public synchronized MyCouponBO resolve(MyCouponPO myCouponPO) { if (this.cache.containsKey(myCouponPO)) { return this.cache.get(myCouponPO); } MyCouponBO myCouponBO = new MyCouponBOImpl(myCouponPO, getContext()); cache.put(myCouponPO, myCouponBO); return myCouponBO; } /** * for the given parameters a new MyCoupon will be created */ @Override public MyCouponBO createMyCouponBOByEmail(String email) { // you can validate the code and so on MyCouponPO myCouponPO = this.myCouponPOFactory.create(getDomain(), email, generateCode(), new java.util.Date()); MyCouponBO myCouponBO = this.resolve(myCouponPO); cache.put(myCouponPO, myCouponBO); return myCouponBO; } private String generateCode() { byte[] array = new byte[7]; new Random().nextBytes(array); return new String(array, Charset.forName("UTF-8")); } @Override public int countMyCouponsByEmail(String email) { @SuppressWarnings("unchecked") final Collection<MyCouponPO> myCouponPOs = this.myCouponPOFactory .getObjectsBySQLWhere("email = ? ", new String[] { email }); return myCouponPOs.size(); } /** * Generates a coupon code * @return the generated coupon code */ @Override public MyCouponBO getMyCouponBOByCode(String couponCode) { @SuppressWarnings("unchecked") final ORMObjectCollection<MyCouponPO> myCouponPOs = this.myCouponPOFactory .getObjectsBySQLWhere("coupon=?", new Object[] { couponCode }); if (myCouponPOs.isEmpty()) { return null; } return resolve(myCouponPOs.iterator().next()); } }
Our repository is ready. However, at this stage it is not wired via component framework and can still not be used.
Create a new factory MyCouponBORepositoryExtensionFactory. This factory will be used in our component framework later.
package com.intershop.support.component.mycoupon.internal; import javax.inject.Inject; import com.google.inject.Injector; import com.intershop.beehive.businessobject.capi.BusinessObjectExtension; import com.intershop.component.repository.capi.AbstractDomainRepositoryBOExtensionFactory; import com.intershop.component.repository.capi.RepositoryBO; import com.intershop.support.component.mycoupon.capi.MyCouponBORepository; /** * Factory class for the {@link MyCouponBORepository}. */ public class MyCouponBORepositoryExtensionFactory extends AbstractDomainRepositoryBOExtensionFactory { @Inject private Injector injector; @Override public BusinessObjectExtension<RepositoryBO> createExtension(RepositoryBO repository) { MyCouponBORepositoryImpl myCouponBORepositoryImpl = new MyCouponBORepositoryImpl(MyCouponBORepository.EXTENSION_ID, repository); injector.injectMembers(myCouponBORepositoryImpl); return myCouponBORepositoryImpl; } @Override public String getExtensionID() { return MyCouponBORepository.EXTENSION_ID; } }
Extend the ApplicationBO, so the new business functionality is also available for ISML and Pipeline:
<isset name="MyCouponBORepository" value="#ApplicationBO:Extension("MyCouponBORepository")#" scope="request"/>
or the Java context:
applicationBO.getExtension("MyCouponBORepository")
It is also already possible to create the two files implementations.component and instances.component. However, this is shown in the next section, Wiring of our New Business Object Factories via Component Framework.
Extend the ApplicationBO with the following classes:
package com.intershop.support.component.mycoupon.capi; import com.intershop.beehive.businessobject.capi.BusinessObjectExtension; import com.intershop.component.application.capi.ApplicationBO; /** * Extension for the {@link ApplicationBO}. * </br> * Example for usage: * <pre> * public class MyCouponHandler * { * @Inject * CurrentApplicationBOProvider applicationProvider; * * private MyCouponBORepository getMyCouponBORepository() * { * MyCouponBORepository myCouponBORepository = applicationProvider.getExtension(MyCouponBORepository.class); * } * * public MyCouponBO getMyCouponBOByCode(String couponCode) * { * return getMyCouponBORepository().getMyCouponBOByCode(couponCode); * } * } * </pre> */ public interface ApplicationBOMyCouponExtension extends BusinessObjectExtension<ApplicationBO> { /** * The ID of the created extensions which can be used to get them from the business object later. */ public static final String EXTENSION_ID = "MyCoupon"; /** * Returns the {@link MyCouponBORepository}. * * @return The {@link MyCouponBORepository}. */ public MyCouponBORepository getMyCouponBORepository(); }
package com.intershop.support.component.mycoupon.internal; import com.intershop.beehive.businessobject.capi.AbstractBusinessObjectExtension; import com.intershop.beehive.businessobject.capi.MissingExtensionException; import com.intershop.component.application.capi.ApplicationBO; import com.intershop.support.component.mycoupon.capi.ApplicationBOMyCouponExtension; import com.intershop.support.component.mycoupon.capi.MyCouponBORepository; public class ApplicationBOMyCouponExtensionImpl extends AbstractBusinessObjectExtension<ApplicationBO> implements ApplicationBOMyCouponExtension { public ApplicationBOMyCouponExtensionImpl(String extensionID, ApplicationBO extendedObject) { super(extensionID, extendedObject); } @Override public MyCouponBORepository getMyCouponBORepository() { MyCouponBORepository myCouponBORepository = getExtendedObject() .getRepository(MyCouponBORepository.EXTENSION_ID); if (myCouponBORepository == null) { throw new MissingExtensionException("MyCouponBORepository " + MyCouponBORepository.class); } return myCouponBORepository; } }
Create the factory ApplicationBOMyCouponExtensionFactory class in the same manner as class MyCouponBORepositoryExtensionFactory:
package com.intershop.support.component.mycoupon.internal; import com.intershop.beehive.businessobject.capi.AbstractBusinessObjectExtensionFactory; import com.intershop.beehive.businessobject.capi.BusinessObjectExtension; import com.intershop.component.application.capi.ApplicationBO; import com.intershop.support.component.mycoupon.capi.ApplicationBOMyCouponExtension; /** * The interface describes an extension of the application BO which is intended to retrieve the BO repository of the * promotion code. */ public class ApplicationBOMyCouponExtensionFactory extends AbstractBusinessObjectExtensionFactory<ApplicationBO> { @Override public BusinessObjectExtension<ApplicationBO> createExtension(ApplicationBO application) { return new ApplicationBOMyCouponExtensionImpl(ApplicationBOMyCouponExtension.EXTENSION_ID, application); } @Override public Class<ApplicationBO> getExtendedType() { return ApplicationBO.class; } }
To set up the wiring of our new business object factories via the component framework, delegate the factory initialization to the injection framework called component framework:
<components xmlns="http://www.intershop.de/component/2010" scope="global"> <implementation name="MyCouponBORepositoryExtensionFactory" implements="BusinessObjectExtensionFactory" class="com.intershop.support.component.mycoupon.internal.MyCouponBORepositoryExtensionFactory" /> <implementation name="ApplicationBOMyCouponExtensionFactory" class="com.intershop.support.component.mycoupon.internal.ApplicationBOMyCouponExtensionFactory"> </implementation> </components>
<components xmlns="http://www.intershop.de/component/2010"> <fulfill requirement="extensionFactory" of="com.intershop.beehive.core.capi.businessobject.BusinessObjectExtensionFactories"> <instance with="MyCouponBORepositoryExtensionFactory" /> <instance with="ApplicationBOMyCouponExtensionFactory" /> </fulfill> </components>
After server start the ApplicationBO lists the new extension ApplicationBOMyCouponExtension.
Before we can use our new persistent object and create REST calls that use our new business object, we need to create two files for the DBInit and DBMigrate processes.
Create two files dbinit.properties and dbmigrate.properties:
pre.Class1 = com.intershop.beehive.core.dbinit.preparer.database.DatabaseTablesPreparer
Class1 = com.intershop.beehive.core.dbmigrate.preparer.database.DatabaseTablesPreparer Class2 = com.intershop.beehive.core.dbmigrate.preparer.database.CartridgeDatabaseConstraintsPreparer Class3 = com.intershop.beehive.core.dbmigrate.preparer.database.CartridgeDatabaseIndexesPreparer
Modify apps.component by adding the new business cartridge to the existing intershop.B2CResponsive.Cartridges instance.
<components xmlns="http://www.intershop.de/component/2010"> <!-- ************************************************************************************ --> <!-- * Application Type "intershop.B2CResponsive" * --> <!-- ************************************************************************************ --> <instance name="intershop.B2CResponsive.Cartridges" with="CartridgeListProvider"> <fulfill requirement="selectedCartridge" value="support_bc_mycoupon"/> ... </instance>
Change the build.gradle file for your assembly and add the new cartridge.
def storefrontCartridges = [ 'app_sf_base_cm', 'app_sf_pwa_cm', 'app_sf_responsive', 'app_sf_responsive_cm', 'app_sf_responsive_b2c', 'app_sf_responsive_smb', 'as_responsive', 'app_sf_responsive_b2b', 'app_sf_responsive_costcenter', 'as_responsive_b2b', 'app_sf_responsive_gdpr', 'app_sf_responsive_rma', 'ac_oidc_sf_responsive ', 'ac_oidc_sf_responsive_smb', 'ac_oidc_sf_responsive_b2b', 'support_bc_mycoupon'
Run the following commands to create a new table MyCoupon:
gradlew clean publish deployServer
gradlew <your assembly>:dbinit
or gradlew <your assembly>:dbmigrate
to create your new table MyCoupon.
If the wiring, publishing and the deploy server task were successful, there should be no errors when starting the server.
We suggest that you download the cartridges and integrate it as blueprint on your test environment and for further development.
The following snippet from the build.gradle file demonstrates the usage of your new business cartridge in context of our new REST API:
apply plugin: 'java-cartridge' apply plugin: 'static-cartridge' intershop { displayName = 'Application - My Coupon REST API' } dependencies { compile project(':support_bc_mycoupon') ... }
We define the resource class as an item resource. Thus we use the AbstractRestResource. For collections use the base class AbstractRestCollectionResource instead.
Create the following resource:
package com.intershop.support.app_sf_mycoupon_rest.capi.resource; import javax.ws.rs.POST; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.container.ResourceContext; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import com.google.inject.Inject; import com.intershop.beehive.core.internal.opexpression.parser.ParseException; import com.intershop.component.application.capi.CurrentApplicationBOProvider; import com.intershop.component.rest.capi.RestException; import com.intershop.component.rest.capi.openapi.OpenAPIConstants; import com.intershop.component.rest.capi.resource.AbstractRestResource; import com.intershop.component.rest.internal.RestFrameworkConstants; import com.intershop.support.app_sf_mycoupon_rest.capi.resourceobject.MyCouponRO; import com.intershop.support.component.mycoupon.capi.ApplicationBOMyCouponExtension; import com.intershop.support.component.mycoupon.capi.MyCouponBO; import com.intershop.support.component.mycoupon.capi.MyCouponBORepository; 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.tags.Tag; @Tag(name = "Experimental", description = " ") @OpenAPIDefinition(extensions = @Extension( properties = { @ExtensionProperty(name = OpenAPIConstants.API_ID, value = "promotion") })) public class MyCouponResource extends AbstractRestResource { private static final String MY_COUPON = "mycoupon"; @Inject private CurrentApplicationBOProvider currentApplicationBOProvider; public MyCouponResource() { super(); setName(MY_COUPON); } /** * Returns for a given email a coupon. */ @Operation(summary = "Creates a coupon.") @POST @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML }) public Response generateMyCoupon(@QueryParam("email") String email) throws ParseException { // 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 ApplicationBOMyCouponExtension applicationBOMyCouponExtension = currentApplicationBOProvider.get() .getExtension(ApplicationBOMyCouponExtension.EXTENSION_ID); MyCouponBORepository myCouponBORepository = applicationBOMyCouponExtension.getMyCouponBORepository(); // create a coupon MyCouponBO myCouponBO = myCouponBORepository.createMyCouponBOByEmail(email); // validate 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"); } MyCouponRO myCouponRO = new MyCouponRO(myCouponBO); // make non-cacheable setCacheExpires(RestFrameworkConstants.CACHE_EXPIRY_NOCACHE); return getResponseBuilder().entity(myCouponRO) .build(); } @Override public MyCouponResource getRequestSpecificCopy(ResourceContext rc) { MyCouponResource result = (MyCouponResource)super.getRequestSpecificCopy(rc); return result; } }
Info
Define a new resource object MyCouponRO which uses our new business object MyCouponBO. This class holds the data and will be displayed.
package com.intershop.support.app_sf_mycoupon_rest.capi.resourceobject; import javax.xml.bind.annotation.XmlRootElement; import com.fasterxml.jackson.annotation.JsonTypeName; import com.intershop.component.rest.capi.resourceobject.AbstractResourceObject; import com.intershop.support.component.mycoupon.capi.MyCouponBO; import io.swagger.v3.oas.annotations.media.Schema; @XmlRootElement(name = MyCouponRO.TYPENAME) @JsonTypeName(value = MyCouponRO.TYPENAME) /** * This is the resource object for my coupon code. */ public class MyCouponRO extends AbstractResourceObject { public final static String TYPENAME = "MyCoupon"; public final static String NAME = "MyCoupon"; private String code; public MyCouponRO(MyCouponBO myCouponBO) { super(NAME); setCode(myCouponBO.getCode()); } /** * @return the coupon code */ @Schema(description = "The coupon code") 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; } }
Currently there are two ways to wire a new REST API: via the component framework, see Cookbook - REST Framework, or via Guice, see Cookbook - REST Framework (Guice Based).
The following example shows wiring via the component framework.
If a Google Guice approach is more interesting please follow the Guide - Add New REST Resource from Scratch via Guice (valid to 7.10)
contracts.component
implementations.component
instances.component
<components xmlns="http://www.intershop.de/component/2010" scope="global"> <contract name="MyCouponResource" class="com.intershop.support.app_sf_mycoupon_rest.capi.resource.MyCouponResource"/> </components>
<components xmlns="http://www.intershop.de/component/2010"> <implementation name="MyCouponResource" class="com.intershop.support.app_sf_mycoupon_rest.capi.resource.MyCouponResource" factory="JavaBeanFactory" implements="AbstractRestResource"> <requires name="subResource" contract="RestResource" cardinality="0..n" /> <requires name="name" contract="String" cardinality="1..1" /> </implementation> </components>
<components xmlns="http://www.intershop.de/component/2010"> <fulfill requirement="subResource" of="intershop.B2CWebShop.RESTAPI.root"> <instance name="intershop.B2CWebShop.RESTAPI.MyCouponResource" with="MyCouponResource"> <fulfill requirement="name" value="mycoupon" /> </instance> </fulfill> <fulfill requirement="resourceACLCartridge" value="support_app_sf_rest_mycoupon" of="intershop.B2CWebShop.RESTAPI.AuthorizationService"/> </components>
instances.component
declares that our new REST API will be triggered by the path /mycoupon. Also we have extend the B2C REST API.
An important requirement is to add permission for your new REST call.
Therefore, create a new file next to the instances.component
file:
# Sub-Resource of mycoupon POST|mycoupon{anyPath\:.*}=isAnyUser
Now any user can use the new REST API. For details, refer to Concept - REST Services Authorization.
To enable the new cartridge, modify apps.component by adding the new business cartridge to the existing intershop.B2CResponsive.Cartridges instance:
<components xmlns="http://www.intershop.de/component/2010"> <!-- ************************************************************************************ --> <!-- * Application Type "intershop.B2CResponsive" * --> <!-- ************************************************************************************ --> <instance name="intershop.B2CResponsive.Cartridges" with="CartridgeListProvider"> <fulfill requirement="selectedCartridge" value="support_bc_mycoupon"/> <fulfill requirement="selectedCartridge" value="support_app_sf_rest_mycoupon"/> ... </instance>
We have extend the B2C context. To verify that the new REST API is visible and working as designed, do the following:
Log in to the SMC and choose Site Management.
This displays a list of all sites.
Click on your channel.
Click Try it out.
Enter a test email into the email field and click Execute.
In case of success the response looks like the following screen:
The response code is 200. The request was successful and we are able to create POST requests and generate mycoupon codes via our new REST API.