The present concept is addressed to developers who want to make use of Intershop's REST framework.
The Intershop platform offers simple-to-use lightweight WebService APIs for the integration into external systems. The REST framework serves as the foundation for those APIs.
RESTful web services are services that are built to work best on the web. Representational State Transfer (REST) is an architectural style that specifies constraints, such as the uniform interface, that if applied to a web service induce desirable properties, such as performance, scalability, and modifiability, that enable services to work best on the web. In the REST architectural style, data and functionality are considered resources, and these resources are accessed using Uniform Resource Identifiers (URIs), typically links on the web. The resources are acted upon by using a set of simple, well-defined operations.
( http://java.sun.com/javaee/6/docs/tutorial/doc/gijqy.html )
The central concept of a REST API is a resource. A resource represents a business object like catalog, product etc. and is mapped to a URI. By invoking HTTP methods like GET
and POST
on the URI, it is possible to perform certain operations on that resource. The HTTP methods are mapped to the typical CRUD operations (a 1:1 mapping is not possible).
A REST-based web service offers advantages over traditional WSDL/SOAP-based web services:
The REST framework acts as an additional access layer on top of the existing application layer. It is made available by a single cartridge rest
. The rest
cartridge has only absolutely essential dependencies (cartridges tools
, core
, app
, and component
). Specific APIs (back office, storefront, integration services, customer-specific etc.) can be defined on top of the REST layer.
Term | Explanation |
---|---|
Create, Read, Update, Delete are the four basic functions of persistent storage. | |
Resource | A server-side object that handles the request and is identified by the URI |
ResourceObject (RO) | A serialized/deserialized data object used to transfer structured data between client and server |
Cookbooks about this topic:
The RESTful WebService should be as close as possible to the definition of REST. This is achieved by following these principles:
GET, POST, PUT, DELETE, OPTIONS
).Accept
header (e.g. text/xml
).An Intershop REST URI is constructed according to the following pattern:
http://<web-server>:<port>/INTERSHOP/rest/WFS/<SiteID>/<AppUrlID>;loc=en_US;cur=USD/<Top-level Resources>
The URI uses the new .../rest/...
as part of the URL. Thereby, the Web Adapter recognizes REST requests and handles them in a specific way (e.g., allowing PUT
and DELETE
methods).
Each part of a REST URI should be either a resource, a resource collection, or (in case of GET
requests) a parameter (query or matrix). Collection resources are named with plural nouns, e.g., sites, categories, or products. Resources are named using a locally unique ID, e.g., /sites/<siteID>/categories/<categoryID>
. This also means that elements of a collection resource are accessed by appending their key/name to the collection resource URI, i.e. a specific category is accessed via /categories/<categoryID>
, not /category/<categoryID>
. /category
does not represent a resource itself.
The parameters in a URI can be query parameters or matrix parameters. Matrix parameters have several advantages compared with query parameters:
As all resources are requested via the root resource and subsequent sub-resource lookup using the component framework, the RootResource
provided by the REST framework handles the matrix parameters loc
and cur
and puts the corresponding values into the AppContext / Request dictionary as current locale and current currency. Sub-resources can simply use these values. It is possible to override currency or locale at sub-resource level by simply adding the corresponding matrix parameters at that point in the URI. The abstract base class for all sub-resources parses these parameters and overwrites the corresponding values set by the root resource. Using this approach, each resource can be used to define currency and locale for all its sub-resources. Whether or not specific APIs support the override mechanism is subject to API design.
The response format is chosen by the server in response to the client's HTTP Accept
header (cf. RFC 2616, Sec.14 ).
Basically, this means that a client sending Accept: text/xml
gets the response in XML format and a client sending Accept: application/json
gets the response in JSON. If sending multiple values, the server selects its preferred of the supported formats.
If the server cannot provide the needed format, it returns the HTTP status code 406
.
HTTP methods represent the actions that are to be applied to a resource. The following table gives a short overview of the most important actions. For more details, see RFC 2616, Sec.9 .
Method | Purpose | CRUD equivalent |
---|---|---|
| Retrieves whatever information (in the form of an entity) is identified by the Request-URI. If the Request-URI refers to a data-producing process, it is the produced data which shall be returned as the entity in the response | Read |
| Requests that the origin server accepts the entity enclosed in the request as a new subordinate of the resource identified by the Request-URI in the Request-Line | Create |
| Requests that the enclosed entity is stored under the supplied Request-URI. If the Request-URI refers to an already existing resource, the enclosed entity should be considered as a modified version of the one residing on the origin server. | Update |
| Requests that the origin server deletes the resource identified by the Request-URI | Delete |
| Requests information about the communication options available on the request/response chain identified by the Request-URI |
Note
The methods POST
and PUT
are often confused since POST
has been used for years to achieve the results of PUT
on the web.
( RFC 2616, Sec.9.6 )The fundamental difference between the POST and PUT requests is reflected in the different meaning of the Request-URI. The URI of a POST request identifies the resource that will handle the enclosed entity. That resource might be a data-accepting process, a gateway to some other protocol, or a separate entity that accepts annotations. In contrast, the URI in a PUT request identifies the entity enclosed with the request – the user agent knows what URI is intended and the server MUST NOT attempt to apply the request to some other resource.
Note
The OPTIONS
request is automatically handled by the framework. It returns all HTTP methods for which a resource method is registered at this URI with the correct content types (HTTP Accept and HTTP Content-Type).
GET
and HEAD
are safe methods: They do not change anything on the server side. They can be called multiple times for the same URI and do not have any effects than returning (maybe) different values.
GET
, HEAD
, PUT
, DELETE
, OPTIONS
, and TRACE
are idempotent methods: They may have server side effects, but can still be called multiple times for the same resource without negative effects. For example, the same change to the same resource can be done multiple times; the state after that will be the same as after the first change (e.g., x=1
is the same as x=1;x=1;...;x=1
). An HTTP PUT
in Java corresponds to something like map.put(key, value)
with the URI (or parts of it) being the key and the transferred data being the value.
In contrast, POST
is a non-idempotent operation: In Java, it corresponds to something like list.add(value)
with the transferred data being the value. It can have undesired effects when doing that multiple times.
When an error occurs, an appropriate HTTP status code (see W3C RFC2616 ) is returned. This can be achieved by manually calling addResponseData
at the current resource or by simply throwing a RestException
with the appropriate status code.
For all operations, the most relevant status codes are:
HTTP status code | Meaning | Description | Automatically generated? |
---|---|---|---|
200 | OK | The request has succeeded. | Yes |
201 | Created | The request has been fulfilled and resulted in a new resource being created. The newly created resource can be referenced by the URI(s) returned in the entity of the response, with the most specific URI for the resource given by a location header field. The response should include an entity containing a list of resource characteristics and location(s) from which the user or user agent can choose the one most appropriate. | No |
204 | No content | The request has succeeded, but has no response data. | No |
303 | See other | The response to the request can be found under a different URI and should be retrieved using a | No |
400 | Bad Request | The request could not be understood by the server due to malformed syntax. The client should not repeat the request without modifications. | No |
401 | Unauthorized | The request requires user authentication. The response must include a WWW-Authenticate header field ( section 14.47 ) containing a challenge applicable to the requested resource. The client may repeat the request with a suitable Authorization header field ( section 14.8 ). If the request already included authorization credentials, then the 401 response indicates that authorization has been refused for those credentials. | No |
403 | Forbidden | The server understood the request, but is refusing to fulfill it. Authorization will not help and the request should not be repeated. | No |
404 | Not found | The server has not found anything matching the Request-URI. No indication is given of whether the condition is temporary or permanent. This status code is commonly used when the server does not wish to reveal exactly why the request has been refused, or when no other response is applicable. | No |
405 | Method not allowed | The method specified in the Request-Line is not allowed for the resource identified by the Request-URI. The response must include an Allow header containing a list of valid methods for the requested resource. | Yes, the Web Adapter generates this status and the corresponding "allow" header. At the moment, only |
406 | Not acceptable | The resource identified by the request is only capable of generating response entities which have content characteristics not acceptable according to the accept headers sent in the request. | Yes |
409 | Conflict | The request could not be completed due to a conflict with the current state of the resource. This code is only allowed in situations where it is expected that the user may be able to resolve the conflict and resubmit the request. The response body should include enough information for the user to recognize the source of the conflict. Ideally, the response entity would include enough information for the user or user agent to fix the problem; however, this may not be possible and is not required. Conflicts are most likely to occur in response to a PUT request. | No |
500 | Internal Server Error | The server encountered an unexpected condition which prevented it from fulfilling the request. | Yes |
In addition, operations may have specific status codes.
The REST framework provides interfaces and base classes that provide a way to look up a REST API and its resources via the component framework. Any specific API needs to declare the resources and their relationships (parent-child) for the component framework (see the following chapter Concept - REST Framework#Configuration). Thus, the API-specific resources can be reduced to handle their specific requests.
Any REST API needs to be accessed by a URI of the form http://<http-server>:<port>/rest/<siteID>/<appID>/<resourceNames>
.
The AppID is defined in the apps.component
file of the API's cartridge:
<?xml version="1.0" encoding="UTF-8"?> <components xmlns="http://www.intershop.de/component/2010"> <instance name="intershop.RestPoC.Cartridges" with="CartridgeListProvider"> <fulfill requirement="selectedCartridge" value="rest"/> <!-- include the cartridges of another cartridge list provider --> <fulfill requirement="parent" with="intershop.B2CWebShop.Cartridges"/> </instance> <fulfill requirement="app" of="AppEngine"> <instance with="ApplicationType"> <fulfill requirement="id" value="backoffice"/> <fulfill requirement="namedObject" with="backoffice-rest-api"/> <fulfill requirement="cartridgeListProvider" with="intershop.RestPoC.Cartridges"/> </instance> </fulfill> </components>
The resources and their hierarchy (the part of the API independent from the used HTTP methods) is declared in the instances.component
file:
<?xml version="1.0" encoding="UTF-8"?> <components xmlns="http://www.intershop.de/component/2010"> <instance name="backoffice-rest-api" with="NamedObject"> <fulfill requirement="name" value="rest-api" /> <fulfill requirement="object"> <instance with="RootResource"> <fulfill requirement="name" value="rest-api" /> <fulfill requirement="authenticationProvider"> <instance with="TokenAuthenticationProvider"> <fulfill requirement="loginProvider"> <instance with="SimpleLoginProvider" /> </fulfill> <fulfill requirement="encryptionPassword" value="topsecret" /> <!--optional: fulfill requirement="encryptionAlgorithm" value="..." /--> </instance> </fulfill> <fulfill requirement="prefixPipelineName" value="PrefixREST" /> <fulfill requirement="prefixPipelineStartNode" value="Start" /> <fulfill requirement="defaultCacheTTL" value="86400" /> <fulfill requirement="subResource"> <instance with="CategoryResource"> <fulfill requirement="name" value="categories" /> <fulfill requirement="handler"> <instance with="CategoryHandlerImpl" /> </fulfill> <fulfill requirement="subResource"> <instance with="ProductResource"> <fulfill requirement="name" value="products" /> <fulfill requirement="handler"> <instance with="ProductHandlerImpl" /> </fulfill> </instance> </fulfill> </instance> <instance with="JobsResource"> <fulfill requirement="name" value="jobs" /> <fulfill requirement="itemResource"> <instance with="JobItemResource" /> </fulfill> </instance> <instance with="LocalizedTextResource"> <fulfill requirement="name" value="localizedTexts" /> </instance> <instance with="RegionalSettingResource"> <fulfill requirement="name" value="regionalSettings" /> </instance> <instance with="UserResource"> <fulfill requirement="name" value="users" /> </instance> </fulfill> </instance> </fulfill> </instance> </components>
The component framework automatically creates one instance of each of the defined resources.
It is easy to see that the above API has the following URI paths: /categories
/categories/.../products
/jobs
/localizedTexts
/users
The structure is easy to alter/extend by customized APIs.
The resource handling /jobs
is separated into a collection resource JobsResource
extending AbstractRestCollectionResource
and an item resource JobItemResource
. The framework will automatically direct all calls to /jobs
to the JobsResource
, and all calls to /jobs/<jobID>
to the JobItemResource
, setting the jobID
as the name of the item resource.
To make it even more customizable, the CategoryResource
and the ProductResource
have definable handlers which are called from pure API methods in the CategoryResource
, e.g., to get business objects. In that way, the internal and external APIs can be separated.
The RootResource
in this example is configured to call PrefixREST-Start
before processing the request. The pipeline dictionary written by this call is available to all subsequent pipeline calls of this request.
The interfaces of all resource classes need to be declared in the contracts.component
file:
<?xml version="1.0" encoding="UTF-8"?> <components xmlns="http://www.intershop.de/component/2010"> <contract name="CategoryResource" class="com.intershop.component.rest.capi.resource.CategoryResource" /> <contract name="CategoryHandler" class="com.intershop.component.rest.capi.resource.CategoryHandler" /> <contract name="ProductResource" class="com.intershop.component.rest.capi.resource.ProductResource" /> <contract name="ProductHandler" class="com.intershop.component.rest.capi.resource.ProductHandler" /> <contract name="JobsResource" class="com.intershop.component.rest.capi.resource.JobsResource" /> <contract name="LocalizedTextResource" class="com.intershop.component.rest.capi.resource.LocalizedTextResource" /> <contract name="UserResource" class="com.intershop.component.rest.capi.resource.UserResource" /> </components>
The implementations are declared in the implementations.component
file:
<?xml version="1.0" encoding="UTF-8"?> <components xmlns="http://www.intershop.de/component/2010"> <implementation name="SimpleLoginProvider" implements="LoginProvider" class="com.intershop.component.rest.example.internal.auth.SimpleLoginProvider" /> <implementation name="CategoryResource" implements="AbstractRestResource" class="com.intershop.component.rest.capi.resource.CategoryResource"> <requires name="subResource" contract="RestResource" cardinality="0..n" /> <requires name="name" contract="String" cardinality="1..1" /> <requires name="handler" contract="CategoryHandler" cardinality="1..1" /> </implementation> <implementation name="ProductResource" implements="AbstractRestResource" class="com.intershop.component.rest.capi.resource.ProductResource"> <requires name="subResource" contract="RestResource" cardinality="0..n" /> <requires name="name" contract="String" cardinality="1..1" /> <requires name="handler" contract="ProductHandler" cardinality="1..1" /> </implementation> <implementation name="CategoryHandlerImpl" implements="CategoryHandler" class="com.intershop.component.rest.internal.resource.CategoryHandlerImpl" /> <implementation name="ProductHandlerImpl" implements="ProductHandler" class="com.intershop.component.rest.internal.resource.ProductHandlerImpl" /> <implementation name="JobsResource" implements="AbstractRestCollectionResource" class="com.intershop.component.rest.capi.resource.JobsResource"> <requires name="itemResource" contract="RestResource" cardinality="1..1" /> <requires name="subResource" contract="RestResource" cardinality="0..n" /> <requires name="name" contract="String" cardinality="1..1" /> </implementation> <implementation name="JobItemResource" implements="RestResource" class="com.intershop.component.rest.capi.resource.JobItemResource"> <requires name="subResource" contract="RestResource" cardinality="0..n" /> </implementation> <implementation name="LocalizedTextResource" implements="AbstractRestResource" class="com.intershop.component.rest.capi.resource.LocalizedTextResource"> <requires name="subResource" contract="RestResource" cardinality="0..n" /> <requires name="name" contract="String" cardinality="1..1" /> </implementation> <implementation name="UserResource" implements="AbstractRestResource" class="com.intershop.component.rest.capi.resource.UserResource"> <requires name="subResource" contract="RestResource" cardinality="0..n" /> <requires name="name" contract="String" cardinality="1..1" /> </implementation> </components>
The siteID and appID are used by the RestDispatcher
to find the root resource of the specific REST API. The RestDispatcher
requests a named object with the name rest-api
from the app. If that is found, it is used as the root resource.
The root resource is used as the starting point for a sequential lookup of the resource names of the URI, where each resolved resource is asked to find the matching sub-resource for the next resource name. Again, the component framework is used for that lookup. If a sub-resource inherits from AbstractRestCollectionResource
, it directly requests to the appropriate item resource (defined via its itemResource
requirement), which handles the resolving of a single item, and which can route the request to its sub-resources.
The returned resource implementations are components that are not request-specific, yet Jersey's strength is easy modeling request-specific objects by using annotations; the returned resources are copied (including any attributes set by the component framework) and enriched with request context. Only those copies are used.
c.i.c.rest.capi.RestException
Throwing a RestException
is the preferred way to notify the client about errors. The RestException
itself is only a more convenient way to use the WebApplicationException
. The main differences are:
DEBUG
There are three constructors for this:
/** * Create a new instance with a specified statusCode */ public RestException(int statusCode) {...} /** * Create a new instance with the default statusCode and a custom message */ public RestException(String message) {...} /** * Create a new instance with a specified statusCode, a custom message, and a set of response headers. */ public RestException(int statusCode, String message, Map<String, String> headers) {...}
For many errors, the correct HTTP status code has to be known and returned, and the response has to include certain mandatory headers and/or body content.
To enable developers to implement consistent error handling in their APIs with as little room for error as possible, the class RestException
provides builder methods to return the correct error response for certain use cases.
/** * RFC2616: "The server has not found anything matching the Request-URI. " */ public RestException notFound() {...} /** * RFC2616: * "The request could not be completed due to a conflict with the current state of the resource. This code is only allowed in situations where it is expected that the user may be able to resolve the conflict and resubmit the request. The response body should include enough information for the user to recognize the source of the conflict." */ public RestException conflict() {...} /** * RFC2616: * "The request could not be understood by the server due to malformed syntax. The client SHOULD NOT repeat the request without modifications." */ public RestException badRequest() {...} /** * This exception indicates that the request could not be fulfilled due to invalid attributes contained in the * request body. It will return a status code 400 (Client error/Bad request) and include the invalid attributes. * * @param invalidAttributeNames * The names of the attributes that contained invalid values. */ public RestException invalidAttributes(List<String> invalidAttributeNames) {...} /** * This exception indicates that the request could not be fulfilled due to required attributes missing from the * request body. It will return a status code 400 (Client error/Bad request) and include the missing attribute names. * * @param missingAttributeNames * The names of the attributes that were missing values. * @return */ public RestException missingAttributes(List<String> missingAttributeNames) {...} /** * Add a localization key to the exception which can be used by the client to look-up localized error messages. * * @param key * @return */ public RestException localizationKey(String key) {...}
The RestResponseBuilder
provides methods to ensure consistent, valid responses for certain server conditions. In this case, the conditions do not represent errors, and the response may still be modified before it is sent to the client. Classes derived from AbstractRestResource
can get the response builder by calling getResponseBuilder()
.
It is especially useful to avoid repetitive, error-prone code when sending e.g. a "Resource created" response (status code has to be 201 (Created)
, header Location
has to be set, response body should contain a link representation pointing to the created response).
/** * RFC 2616: * "The request has been fulfilled and resulted in a new resource being created. The newly created resource can be referenced by the URI(s) returned in the entity of the response, with the most specific URI for the resource given by a location header field." * * @param uri * The location of the created resource. */ public static Response created(String uri); /** * RFC 2616: * "The request has been fulfilled and resulted in a new resource being created. The newly created resource can be referenced by the URI(s) returned in the entity of the response, with the most specific URI for the resource given by a location header field." * * @param linkRO * The LinkRO containing the location of the created resource and additional optional information. */ public static Response created(LinkRO linkRO); /** * RFC 2616: * "The response to the request can be found under a different URI and should be retrieved using a GET method on that resource." * * @param uri * The location of the created resource. */ public static Response seeOther(String uri); /** * RFC 2616: * "The response to the request can be found under a different URI and should be retrieved using a GET method on that resource." * * @param linkRO * The LinkRO containing the location of the created resource and additional optional information. */ public static Response seeOther(LinkRO linkRO); /** * RFC 2616: "The request has succeeded, but has no response data." */ public static Response noContent();
Usage:
@POST public Response createBasket() { ... return getResponseBuilder().created(new LinkRO(basketRO.getBasketID(), basketURI)).build(); }
A PipelineCall
is a convenient way to access pipeline logic from REST resources.
The most important methods are:
public PipelineCall(String pipelineName, String pipelineStartnodeName, Class<?> resultType, String resultName) {...} /** * Invokes the specified pipeline using CurrentDomain and CurrentRequest * * @return */ public Object invoke() {...} /** * Set a parameter. If value is null and a parameter with this name exists, * it will be removed. */ public void setParameter(String name, Object value) {...} /** * Set whether an exception will be thrown, if the result cannot be found under the given resultName in the PipelineDictionary after an invoke(). */ public void setResultMandatory(boolean resultMandatory) {...}
It is usually used like this:
PipelineCall pipelineCall = new PipelineCall("RESTProcessCart", "GetCart", B2CBasketBO.class, "CurrentCartBO"); pipelineCall.setParameter("BasketID", basketID); pipelineCall.setParameter("RequireCurrentCartBO", 1); b2cBasketBO = (B2CBasketBO)pipelineCall.invoke();
The AbstractRestResource
is the base class for all resources in an Intershop REST API. It automatically handles the lookup and instantiation of sub-resources, and provides some methods and properties relevant to sub-resources.
The most important methods and properties are:
/** * The current site. */ protected Domain currentSite; protected PermissionProvider permissionProvider; private RestResource parentResource; /** * Get the current permission context. If no PermissionContext is available, constructs a new one from the known information. */ protected PermissionContext getPermissionContext() {...} /** * @return the combined matrix parameters regardless of where in the * resource hierarchy they are set. Never returns null. */ private MultivaluedMap<String, String> getCombinedMatrixParams() {...} /** * Get the value of a matrix parameter. * * @param name * @param localOnly * If true, only Jersey-compliant, resource-specific matrix * parameters are accepted. If false, all matrix parameters from * the URI are searched. * @return The value of the matrix parameter occurring right-most in the * URI. */ public String getMatrixParamValue(String name, boolean localOnly) {...} /** * Get the name of the resource (i.e. the URI-segment representing this resource) */ public String getName() {...} /** * Get the request-specific instance of this resource. Is often overridden to transfer more data from the component definition to the instance. */ public RestResource getRequestSpecificCopy(ResourceContext rc) {...} /** * Get the parent resource. */ public RestResource getParent() {...} /** * Store data which will be added to the final response before returning it * to the client. * This method can be used if a resource method doesn't return a new Jersey * Response-object but instead a local type. * * @param status * The response status to set * @param headerName * A header to be set. * @param headerValue * The header's value. */ public void addResponseData(int status, String headerName, String headerValue) {...} /** * Store data which will be added to the final response before returning it * to the client. * This method can be used if a resource method doesn't return a new Jersey * Response-object but instead a local type. * * @param status * The response status to set * @param headers * A map of headers and header values to be set. */ public void addResponseData(int status, Map<String, String> headers) {...} /** * Store a single header which will be added to the final response before returning it * to the client. * This method can be used if a resource method doesn't return a new Jersey * Response-object but instead a local type. * @param headers */ private void addResponseHeader(String name, String value) {...} /** * Set the time until which the current response may be cached by the * WebAdapter. * * @param remainingSeconds */ public void setCacheExpires(long remainingSeconds)
The AbstractRestCollectionResource
is the base class for all collection resources in an Intershop REST API. It is derived from the AbstractRestResource
and distinguishes between items and sub-resources (named sub-resources take precedence).
A typical REST API will distinguish between actions on resource collections ( GET
list, POST
item) and actions on single resources ( GET
item, PUT
modified item, DELETE
item). The AbstractRestCollectionResource
helps to keep the declaration and code for these cases separate:
... <fulfill requirement="subResource"> ... <instance with="OrganizationsResource"> <fulfill requirement="name" value="organizations" /> <fulfill requirement="itemResource"> <instance with="OrganizationItemResource"> <fulfill requirement="subResource"> <instance with="UserResource"> <fulfill requirement="name" value="users" /> </instance> </fulfill> </instance> </fulfill> </instance> ... </fulfill> ...
c.i.c.rest.capi.resourceobject.AbstractResourceObject
The AbstractResourceObject
is the base class for all resource objects, i.e. for all data objects that are to be serialized and deserialized.
It is Jackson-annotated to expose only categoryID
public fields and methods. All other fields and methods which should be exposed have to be annotated with @JsonProperty. This behavior will be inherited by all sub-classes.
As a result, a typical RO can be a plain old Java object (POJO) extending AbstractResourceObject
.
c.i.c.rest.capi.resourceobject.ExtensibleResourceObject
The ExtensibleResourceObject
can be used as base class for all resource objects that can have custom, dynamic attributes.
As noted above, using custom attributes when POSTing or PUTting data can be quite difficult (especially when an attribute value is an RO itself), so in most cases, developers should opt for using POJOs derived from AbstractResourceObject
instead.
c.i.c.rest.capi.resourceobject.LinkRO
As REST APIs typically need to return links to resources (e.g., in an item listing), the framework provides a simple class for this purpose.
The LinkRO
has the following attributes:
AbstractResourceObject
c.i.c.rest.capi.resourceobject.ResourceCollectionRO
The ResourceCollectionRO
is a typed ordered list of ROs. In addition, it has properties for paging and sorting information (although such functionality is not provided by the class itself).
Producing Collections
@GET @Produces({ MediaType.APPLICATION_JSON }) public ResourceCollectionRO<BasketLineItemRO> getBasketContent(@QueryParam(BasketConst.PRODUCT_ATTRS) String productAttrs) {...}
{ "elements" : [ { "name" : "basketLineItem", "type" : "BasketLineItem", "id" : "sIIKAB155Z8AAAEw_nQISGna", "displayName" : "1team Automator 4P - DVD duplicator", "total" : { "value" : 1010.04, "available" : true, "currencyMnemonic" : "USD" }, "quantity" : 3, "price" : { "value" : 336.68, "available" : true, "currencyMnemonic" : "USD" }, "promotions" : [ ], "sku" : "2745176", "warranty" : { "name" : "dependentLineItem", "type" : "DependentLineItem", "displayName" : "AppleCare Protection Plan - extended service agreement - 2 years", "price" : { "value" : 176.97, "available" : true, "currencyMnemonic" : "USD" }, "sku" : "2968847", "shortDesc" : "AppleCare Protection Plan - Extended service agreement - parts and labor - 2 years ( 2nd/3rd year )", "longDesc" : "The AppleCare Protection Plan is a uniquely integrated service and support solution that extends the complimentary coverage on your Mac to three years from the computer's purchase date. This comprehensive plan includes expert telephone assistance, onsite repairs for desktop computers, global repair coverage for portable computers and Mac mini, web-based support resources, and powerful diagnostic tools - all for one economical price." }, "shortDesc" : "1team Automator 4P - DVD duplicator - external", "thumbnail" : "PrimeTech-PrimeTechSpecials-Site/-/PrimeTech/en_US/PrimeTech/thumbnails/T311716.jpg", "longDesc" : "CD Team is specializing in the supply of CD and DVD discs, duplication equipment and services. With discs, its services range from the straight forward supply of discs right through to complex duplication, bespoke packaging, fulfillment and delivery work. PRODUCT FEATURES: Remote network software for both Mac & PC; Standalone system; Simple to use; Internal hard disk to store multiple images; Fitted with DVD-R/DVD+R and CD-R combo recorders; Batch mode - load multiple masters and walk away; Full color thermal printer; Display shows status of recording, type of master etc; Compare master option mode allows you to verify your copied CD or DVD for complete data integrity.", "attributes" : { } } ], "type" : "ResourceCollection", "name" : "basketLineItems" }
Consuming Collections
{ "elements" : [ { "sku" : "2745176", "quantity" : 3, "warranty" : { "sku" : "2968847" } } ] }
@POST @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public AbstractResourceObject addItemsToBasket(ResourceCollectionRO<BasketLineItemRO> productLines)
Note
When consuming collections, it is strongly recommended to use always a typed ResourceCollectionRO
as input parameter for the methods (use ResourceCollectionRO<LinkRO>
instead of just ResourceCollectionRO
).
The class c.i.c.rest.capi.paging.PagingUtils
provides a method getRangeFromPageable(PageableIterator<?> pageable, Integer offset, Integer amount)
which can be used to retrieve a sub-range from a pageable iterator (useful for dynamic lists based on offsets and amounts instead of pages).
The REST web service can be tested using Intershop's automated test framework. The relevant part of the framework is based on HTMLUnit.
First test cases have been implemented for the B2C WebShop REST API. These tests can be found in the package tests.features.com.intershop.maintenance.rest
.
Tests extending RestTestCase
look like:
public void testGetPromotionList() { headers.put("Accept", "application/json"); result = restTester.doGET("/-/promotions", headers); assertEquals("wrong response code returned", 200, result.getStatusCode()); assertEquals("wrong contentType returned", "application/json", result.getContentType()); assertNotNull("no JSON object returned", result.getJSONObject()); if(result.isArray()) { JsonNode arrayNode = result.getRootNode(); // check all promotions for(int i=0; i<arrayNode.size(); i++) { // get promotion element JsonNode node = arrayNode.get(i); assertNotNull("no name value returned", result.getAttributeValueByNode(node,"name")); assertNotNull("no uri value returned", result.getAttributeValueByNode(node,"uri")); } } }
The framework and APIs based on it can use the standard page caching of the Web Adapter. This includes the support for selective pagecache deletion via keywords or full-text search.
The caching differentiates between responses with different content types, i.e. a request to a resource using Accept: text/xml
does not return a cached JSON-response and vice versa.
GET
The default cache time-to-live (TTL) can be set in the component configuration of the RootResource
(see example instances.component
above):
<fulfill requirement="defaultCacheTTL" value="86400" />
The TTL set here is used for all responses unless specified otherwise in specific (API-)resources.
A resource can define its own cache TTL simply by calling the method (inherited from AbstractRestResource
):
setCacheExpires(1800);
(The example would set the maximum time for caching this response to 1800s, i.e. 30 min).
To prevent a response from being cached at all, setCacheExpires(RestFrameworkConstants.CACHE_EXPIRY_NOCACHE)
has to be called.
REST resources can also specify cache keys (equivalent to the
<ISCACHEKEY ...>
in ISML). A resource can add cache keys to the response by calling the method (inherited from AbstractRestResource
):
addCacheKey(Object key)
The key can be either a String or any object.
Request using the HTTP methods POST
, PUT
, and DELETE
will not be cached.
Several protocols are available for the authentication of clients in the context of RESTful web services:
Initially, the HTTP authentication is sufficient for most authentication purposes.
The OAuth protocol allows a client/app/widget to act on behalf of a particular user (without knowledge of the user's password) and may be important in the context of widgets. This does not fit our current use cases and is therefore disregarded for the time being.
The authentication mechanism can be selected when defining the API (see Concept - REST Framework#Configuration). A first (and default) implementation will be the token-based authentication.
To allow authentication to a stateless system, the client has to authenticate every request to a restricted resource. To avoid resending the password each time, a token-based approach is taken.
When a client sends user credentials, these are validated on the server using standard Intershop mechanisms (the authentication domain is resolved from the SiteID
- part of the request URI; login name and password have to be supplied using the HTTP Authorization
header). If they match an existing user, its ID, login time (==token creation time), and remaining validity time are concatenated. The resulting string will then be encrypted and sent to the client.
The client can simply include the authentication token with its requests. If they can be decrypted successfully and are still valid, the user identified by the decrypted user ID is assumed to have previously been authenticated.
The token is never decrypted by the client or the web adapter.
The handling of the interaction token is quite similar to the session-id of a classic request. In contrast, the token contains all important information for the interaction, so no server-side storage is required.
With the TokenAuthenticationProvider
, the client has the choice to
The TokenAuthenticationProvider
does not perform a login on the server side because the specific login handling is part of the business logic for a specific API. A LoginProvider
can be set using the component framework.
Each token is only valid for a given timespan, which is defined by the maximum inactivity time for the accessed site.
If an invalid token is sent, the server returns 400
(BAD REQUEST). The message is returned in the entity as well as in the header authentication-token: AuthenticationTokenInvalid
(instead of the token value).
If an outdated token is sent, the request is processed as an anonymous request, but the result will include the header authentication-token
with the value AuthenticationTokenOutdated
(instead of the token value). If the request is for a restricted resource, the resource returns status code 401
(Authorization required).
The token-based approach as well as the form-based or HTTP-Basic login require that all communication between client and server is encrypted. Therefore, it must use TLS/https.
For any token-based authentication, a number of security implications/considerations apply. A good overview is given in the OAuth 2.0 Bearer-token specification (sect. 3.3) :
3.3. Summary of Recommendations
Safeguard bearer tokens Client implementations must ensure that bearer tokens are not leaked to unintended parties, as they will be able to use them to gain access to protected resources. This is the primary security consideration when using bearer tokens with OAuth and underlies all the more specific recommendations that follow.
Validate SSL certificate chains The client must validate the TLS certificate chain when making requests to protected resources. Failing to do so may enable DNS hijacking attacks to steal the token and gain unintended access.
Always use TLS (https) Clients must always use TLS (https) when making requests with bearer tokens. Failing to do so exposes the token to numerous attacks that could give attackers unintended access.
Do not store bearer tokens in cookies As cookies are generally sent in the clear, implementations must not store bearer tokens within them.
Issue short-lived bearer tokens Using short-lived (one hour or less) bearer tokens can reduce the impact of one of them being leaked. The User-Agent flow should only issue shortlived access tokens.
Do not pass bearer tokens in page URLs Browsers, web servers, and other software may not adequately secure URLs in the browser history, web server logs, and other data structures. If bearer
tokens are passed in page URLs (typically as query string parameters), attackers may be able to steal them from the history data, logs, or other unsecured locations. Instead, pass browser tokens in message bodies for which confidentiality measures are taken.
The REST framework also returns a token for anonymous requests. In that case, a token contains the ID of the current anonymous user (automatically created). This makes it possible to restrict access to certain resources (e.g., baskets) to the client that created them. The client can identify itself using the authentication token even if it does not represent a registered user.
A use case for this is the creation and modification of baskets (shopping carts): An anonymous user can create a shopping cart, but that cart should only be accessible to the same anonymous user. (In a standard web context, this is achieved by using sessions. In a REST context, there is no session.)
If the response of a REST request comes from a cache, the response will not contain a token. There is no general rule that says if a response can be cached or not, the general rule is that the token is optional.
The authorization framework for the REST framework depends on the standard Intershop authorization approach, which is being reworked at the moment.
Until the new authorization framework is completed, an approach similar to the classic pipeline access control lists (ACLs) is supported. The pipeline ACL scheme cannot simply be used for this, since the permission check would happen too late in the request processing.
Instead, a new file staticfiles\cartridge\components\resources-acl.properties with a syntax almost identical to that of the pipelines-acl.properties will be used:
/=Organization\:PRC_LOGIN_BACKOFFICE organizations-POST=Channel\:NONE;Organization\:PRC_MANAGE_ORGANIZATIONS
For each resource path relative to the API root path (usually .../INTERSHOP/rest/WFS/<siteID>/<appUrlID>
) and HTTP method, a set of permissions can be defined. The HTTP method is optional; a set of permissions specific to a path and a method will take precedence over a set of permissions just for the path.
As all parts of the path will be handled sequentially, a user without permission for a path aaa
will automatically be unable to access any sub resource of that path aaa/bbb
, too.
The permissions and permission processing (right-hand side of each declaration) are identical to the permission handling for pipelines.
The example given above means that:
Only users that are allowed to manage organizations are able to POST
to an organization resource (regardless of where in the resource hierarchy it is located).
Note
If the same resource needs different permissions depending on their location, different names (URI identifiers) have to be used for the resource instances.
Exceptions (that are not handled by a resource implementation itself) are handled by exception mapper(s) as specified by https://jakarta.ee/specifications/restful-ws/3.0/jakarta-restful-ws-spec-3.0.html#exceptionmapper. The ICM's primary exception mapper is com.intershop.component.rest.internal.application.RestExceptionMapper
which handles the exceptions as follows:
Exception | Treated As | Response Code | Logged (Level) |
---|---|---|---|
com.intershop.component.rest.capi.RestException | client error | unchanged | trace without stacktrace |
jakarta.ws.rs.ClientErrorException | client error | unchanged | trace without stacktrace |
org.glassfish.jersey.server.ParamException | client error | 400 |
|
jakarta.ws.rs.WebApplicationException | server error | unchanged | trace without stacktrace |
java.io.EOFException | client error | 400 | trace without stacktrace |
com.intershop.beehive.orm.capi.common.OCAException | client error | 409 | warn without stacktrace |
every other exception | server error | 500 | error with stacktrace |
There are two additional exception mappers which handle special exceptions that may be thrown by the framework during request parsing:
Mapper | Handles |
---|---|
com.intershop.component.rest.internal.application.JsonParseExceptionMapper | com.fasterxml.jackson.core.JsonParseException |
com.intershop.component.rest.internal.application.JsonMappingExceptionMapper | com.fasterxml.jackson.databind.JsonMappingException |
Both mappers respond with HTTP-400
and log the exception (no stacktrace) using the trace
level.