Delivery API 0.9
This pages describes the Delivery API version 0.9, the default version in Bloomreach Experience Manager 14.x.
The Delivery API version 0.9 is no longer supported as of Bloomreach Experience Manager 15.
In Bloomreach Experience Manager 14.3.0 and later, Delivery API v1.0 is available and can optionally be enabled.
In Bloomreach Experience Manager 15, Delivery API v1.0 is the only supported version.
Introduction
The Delivery API (formerly Page Model API) was designed to address the following two factors:
- Intuitive, built-in model contribution and aggregation JSON API
- Seamless integration with WCMS delivery tier and channel management features
A built-in JSON API should represent a generic resource representation, while the integration with a delivery tier framework should represent a dynamic page model, comprised of component representations and their content and domain-specific models, based on flexible page compositions through the Experience manager.
Also, it should be straightforward for developers to contribute any kind of model objects to the aggregated page representation in component implementations, without having to write boilerplate code by themselves.
The Delivery API provides an intuitive REST API endpoint. For example, for a channel served at http://localhost:8080/site/, its Delivery API is available through http://localhost:8080/site/resourceapi/. The JSON API resources represent intuitive models containing all the components, content items, menus, domain specific models, etc. in a generic and effective way. SPAs can consume the API to implement the delivery tier and fully integrate with channel management features as a result.
Developers can also use standard HST APIs to participate in contributing content items or domain-specific models to the aggregated page model representation. See Model Contribution API for details.
Delivery API 0.9
As explained in Configure Delivery API, once you configure the Delivery API, it becomes available to SPAs automatically at the child mount path (e.g, /resourceapi), configured by the @hst:pagemodelapi property. Internally, HST Container automatically augments the mount with a child mount, the name of which is set to the @hst:pagemodelapi property value, for Delivery API. This auto-augmented child mount invokes the PageModelPipeline, which is fairly similar to the default website pipeline, except that it does not invoke the rendering phases (i.e., rendering FreeMarker/JSP templates). Instead it processes contributed model collection, page model aggregation and JSON serialization phases.
This enables SPAs to consume all the models included and aggregated for the page. For example, if the initial page, loading an SPA, is from http://localhost:8080/site/myapp/ or http://localhost:8080/site/myapp/news/, then developers can figure out the API endpoint at http://localhost:8080/site/myapp/resourceapi/ or http://localhost:8080/site/myapp/resourceapi/news/. As it uses the built-in HST Mount configurations, it is also possible to create links in the server-side code by using the standard org.hippoecm.hst.core.linking.HstLinkCreator API.
The Delivery API follows the core principles of HATEOAS. An SPA enters a REST application through a simple HST-2 navigational URL. An aggregated page model is returned for the SPA to discover all the data within the resource representations to construct the page. Example payloads look like the following:
GET /site/resourceapi/ HTTP/1.1 Host: localhost:8080 Accept: application/json ...
The JSON response is always of the following structure:
HTTP/1.1 200 OK Access-Control-Allow-Origin: http://localhost:3000 API-Version: 0.9 Content-Type: application/json;charset=UTF-8 Content-Length: ... ... { "id": "r19", "_links": { "self": { "href": "http://localhost:8080/site/resourceapi" }, "site": { "href": "http://localhost:8080/site/" } }, "page": { ... }, "content": { ... } }
The JSON response contains an HTTP body which represents an Aggregated Page Model, described in the next section. On a high level, an Aggregated Page Model contains its identifier, follow-up links, page representation containing components and domain specific models, and content models.
Aggregated Page Model
The Aggregated Page Model, which establishes the root level structure of Delivery API responses, can be depicted in a domain model as follows:
Each domain object is described below.
AggregatedPageModel
This defines the root level object in the Delivery API responses.
Field | Type | Mandatory | Description |
---|---|---|---|
id | String | Yes | The reference namespace ID of the root page component. |
_meta | Map<String,JSON> | No | Metadata map, containing pairs of String key and JSON value. |
_links | Map<String,LinkModel> | No | Follow-up links, containing pairs of link name key and LinkModel value. |
page | JSON | Yes |
The page representation in JSON. This representation follows the same structure as the HST component configuration (ComponentWindowModel) for a page. |
content | Map<String,JSON> | No | Content representation map, containing pairs of JSON Identifier key and content item representation in JSON. Any HST Content Beans, contributed by each HstComponent, for documents, folders, gallery images and assets are included in this content field. |
Unlike other metadata fields, the page field and the content field represent dynamic page composition with components in a page and content item representations contributed by each component in a page.
Also, see Model Contribution API for details on how each component can contribute content items to content field.
LinkModel
This defines a generic container of the follow-up link objects for a linkable resource representation.
Field | Type | Mandatory | Description |
---|---|---|---|
href | String | Yes | The URI of this link. |
type | String | No |
The type of the link. e.g, internal, external or resource. internal means the link is in the same SPA page resources, and so SPA may retrieve the linked resource through AJAX (XHR) calls, without having to reload the whole SPA page. external links cannot be represented within the current SPA, meaning the link should be treated as a document request (normal URL) and should not be fetched as XHR request by the SPA. As of version 14.1.0, external links are fully qualified URLs.
resource links are mostly for binary resources such as images and assets. As of version 14.1.0, resource links are fully qualified URLs.
|
rel | String | No |
The name of the relationship that the linked resource has to the page from which it’s referenced. |
title | String | No | The title of the link. |
ComponentWindowModel (HST Component Configuration)
This defines either a page component (which is a composite representation of HST Component Configurations) or a single descendant HstComponent resource representation.
Field | Type | Mandatory | Description |
---|---|---|---|
id | String | Yes |
The reference namespace ID of the component. |
_meta | Map<String,JSON> | No |
Metadata map, containing pairs of String key and JSON value. |
_links | Map<String,LinkModel> | No |
Follow-up links, containing pairs of link name key and LinkModel value. |
name | String | Yes |
HST Component configuration node name. |
componentClass | String | Yes |
HST Component class name. |
type | String | Yes |
HST Component class type. |
label | String | No | HST Component catalog item's label. See Channel Editor Catalog for detail. |
components | Array<ComponentWindowModel> | No | Array containing child copmonent representations, each of which contains data in the same structure of ComponentWindowModel, recursively. |
models | Map<String,JSON> | No | Map of content, menu, domain-specific model representations, etc., containing string key and object value in JSON. Any models, that are contributed by HstComponent but not included in the top level content field, are included in this models field in a component representation. |
Unlike other metadata fields, the components field and the models field represent dynamically composed component representations and content item reference, menu model representations or other domain-specific model representations.
See Model Contribution API for details on how each component can contribute menu, domain-specific models, etc. to models field.
Content vs. Models
As you may have noticed, there are two different kinds of model container fields. One is the top level content field and the other is models in each component representation. Basically both include the models contributed by each HstComponent.
The difference is, however, the top level content field contains only WCMS content data for documents, folders, gallery images and assets, by default. The other models such as menu, HstURL, HstLink or other domain-specific models are included in the models field of each component representation. When an HstComponent contributes a WCMS content model (for documents, folder, gallery images or assets), the real data of the content model will be included only in the top level content field, while leaving a JSON Pointer reference as JSON String to the real JSON content data in the models field of the component representation. Let's see the simplified example below.
{ // ...SNIP... "page":{ // ...SNIP... "components":[ { "id":"r19_r1", "name":"main", "type":"COMPONENT", "models":{ "document":{ "$ref":"/content/u895fb1b6410d497298946b6a06d2b361" }, "menu":{ "name":"main", "siteMenuItems":[ // ...SNIP.., ] } } } ], // ...SNIP... }, "content":{ "u895fb1b6410d497298946b6a06d2b361":{ "id":"895fb1b6-410d-4972-9894-6b6a06d2b361", "name":"banner1", "displayName":"banner1", "content":{ "name":"hap:content", "displayName":"hap:content", "value":"\n \n\n <p>Banner description</p>\n\n \n " }, "title":"Sample banner", "image":{ "$ref":"/content/ub89d576f680a4bbf9c272dced9da3d6c" }, "address":false, "localeString":"en" }, // ...SNIP... } }
The first component contains two model objects. One is "document" and the other is "menu". Because the first one ("document") represents a WCMS content model, it is included in the top level content field and referenced by a JSON Pointer reference property ("$ref"), whereas the second one ("menu") is embedded inside the models field of the component itself.
This approach gives the following advantages:
- Even if an HST Content Bean object as a model is referred by multiple HstComponents in the same page, the object will not be serialized multiple times. Instead, the real content data will be serialized only once in the top level content field, while each component representation has just a JSON Pointer reference as JSON String to the real content item. This makes JSON serializations a lot more effective.
- An HST Content Bean object could have reference properties to other HST Content Bean objects. In that case, instead of nesting all the referred content data inside a content item representation, the referenced object property will be replaced by a JSON Pointer reference as JSON String as well and the real content data of the referred HST Content Bean object will be separately serialized into the top level content field as a sibling. This makes the JSON structure a lot cleaner.
- The Delivery API module can also avoid any issues caused by circular references between HST Content Bean objects as a result.
Best Practice to Retrieve Model Objects in Application
Even if every HST Content Bean object as a model is referenced by a JSON Pointer reference ("$ref") by default, it is recommended to handle both cases in a generic way in your SPA applications because each case represents the same logical JSON Schema anyway whether it is referenced by a JSON Pointer reference or embedded with the real content.
For example, in JavaScript code, you can check whether or not the model object contains "$ref" field first. If found, you can look up the referenced model object. Otherwise, you can read and use the model object directly. Here's an example JavaScript code snippet:
import jsonpointer from 'jsonpointer'; //... // suppose 'pageModel' is the root JSON object and 'models' represents the 'models' field of a component. let documentWrapper = models.document; let document; if (documentWrapper['$ref']) { // if it has a JSON Pointer reference, then find the real document content through the reference ID, using the JSON Pointer library. let documentRef = documentWrapper['$ref']; document = jsonpointer.get(pageModel, documentRef); } else { // otherwise, the document is embedded inside the models. document = documentWrapper; } // Now you can read the fields of 'document' object... // ...
Check out the Demo Project and its source for more examples.
JSON Response Examples
Let's first take a look around a whole JSON response example from an AggregatedPageModel object below.
{ "id":"r19", "_links":{ "self":{ "href":"/resourceapi" }, "site":{ "href":"http://localhost:8080/site/myapp" } }, "page":{ "id":"r19", "name":"homepage", "componentClass":"org.hippoecm.hst.core.component.GenericHstComponent", "type":"COMPONENT", "components":[ { "id":"r19_r1", "name":"main", "componentClass":"org.hippoecm.hst.core.component.GenericHstComponent", "type":"COMPONENT", "components":[ { "id":"r19_r1_r1", "name":"container", "componentClass":"org.hippoecm.hst.pagecomposer.builtin.components.StandardContainerComponent", "type":"CONTAINER_COMPONENT", "label":"Homepage Main Container", "components":[ { "id":"r19_r1_r1_r1", "name":"banner", "componentClass":"org.onehippo.cms7.essentials.components.EssentialsDocumentComponent", "type":"CONTAINER_ITEM_COMPONENT", "label":"Banner", "models":{ "document":{ "$ref":"/content/u895fb1b6410d497298946b6a06d2b361" } }, "_meta":{ "paramsInfo":{ "document":"banners/banner1" }, "params":{ "com.example.cms7.targeting.TargetingParameterUtil.hide":"off", "document":"banners/banner1", "org.hippoecm.hst.core.component.template":"webfile:/freemarker/hstdefault/essentials-banner.ftl" } }, "_links":{ "componentRendering":{ "href":"/site/myapp/resourceapi?_hn:type=component-rendering&_hn:ref=r19_r1_r1_r1" } } }, { "id":"r19_r1_r1_r2", "name":"banner1", "componentClass":"org.onehippo.cms7.essentials.components.EssentialsDocumentComponent", "type":"CONTAINER_ITEM_COMPONENT", "label":"Banner", "models":{ "document":{ "$ref":"/content/u9a3f1f5c530243c49bec584e810ffa2f" } }, "_meta":{ "paramsInfo":{ "document":"banners/banner2" }, "params":{ "com.example.cms7.targeting.TargetingParameterUtil.hide":"off", "document":"banners/banner2", "org.hippoecm.hst.core.component.template":"webfile:/freemarker/hstdefault/essentials-banner.ftl" } }, "_links":{ "componentRendering":{ "href":"/site/myapp/resourceapi?_hn:type=component-rendering&_hn:ref=r19_r1_r1_r2" } } } ], "_meta":{ "params":{ } }, "_links":{ "componentRendering":{ "href":"/site/myapp/resourceapi?_hn:type=component-rendering&_hn:ref=r19_r1_r1" } } } ], "_meta":{ "params":{ } }, "_links":{ "componentRendering":{ "href":"/site/myapp/resourceapi?_hn:type=component-rendering&_hn:ref=r19_r1" } } }, { "id":"r19_r2", "name":"top-right", "componentClass":"org.hippoecm.hst.core.component.GenericHstComponent", "type":"COMPONENT", "_meta":{ "params":{ } }, "_links":{ "componentRendering":{ "href":"/site/myapp/resourceapi?_hn:type=component-rendering&_hn:ref=r19_r2" } } }, { "id":"r19_r3", "name":"menu", "componentClass":"com.example.cms.components.HapMenuComponent", "type":"COMPONENT", "models":{ "menu":{ "name":"main", "selectSiteMenuItem":{ "depth":0, "repositoryBased":false, "name":"home", "expanded":true, "selected":true, "parameters":{ "css class":"home" }, "childMenuItems":[ ], "_links":{ "site":{ "href":"/site/myapp", "type":"internal" } } }, "siteMenuItems":[ { "depth":0, "repositoryBased":false, "name":"home", "expanded":true, "selected":true, "parameters":{ "css class":"home" }, "childMenuItems":[ ], "_links":{ "site":{ "href":"/site/myapp", "type":"internal" } } }, { "depth":0, "repositoryBased":false, "name":"news", "expanded":false, "selected":false, "parameters":{ "css class":"" }, "childMenuItems":[ ], "_links":{ "site":{ "href":"/site/myapp/news", "type":"internal" } } } ] } }, "_meta":{ "paramsInfo":{ "siteMenu":"main" }, "params":{ "selectedMenu":"on", "level":"1", "menu":"main" } }, "_links":{ "componentRendering":{ "href":"/site/myapp/resourceapi?_hn:type=component-rendering&_hn:ref=r19_r3" } } } ], "_meta":{ "definitionId":"hst:pages/homepage", "params":{ } }, "_links":{ "componentRendering":{ "href":"/site/myapp/resourceapi?_hn:type=component-rendering&_hn:ref=r19" } } }, "content":{ "u895fb1b6410d497298946b6a06d2b361":{ "id":"895fb1b6-410d-4972-9894-6b6a06d2b361", "_links":{ "site":{ "href":"/site/myapp", "type":"internal" } }, "name":"banner1", "displayName":"banner1", "content":{ "name":"hap:content", "displayName":"hap:content", "value":"\n \n\n <p>Banner description</p>\n\n \n " }, "title":"Sample banner", "image":{ "$ref":"/content/ub89d576f680a4bbf9c272dced9da3d6c" }, "address":false, "localeString":"en" }, "ub89d576f680a4bbf9c272dced9da3d6c":{ "id":"b89d576f-680a-4bbf-9c27-2dced9da3d6c", "_links":{ "site":{ "href":"http://localhost:8080/site/binaries/site/binaries/content/gallery/hap/banners/banner-1.png", "type":"resource" } }, "name":"banner-1.png", "displayName":"banner-1.png", "fileName":"banner-1.png", "description":"Description for banner-1.png", "original":{ "width":700, "height":250, "length":151972, "lastModified":1395331380000, "mimeType":"image/png", "_links":{ "site":{ "href":"http://localhost:8080/site/binaries/site/binaries/content/gallery/hap/banners/banner-1.png", "type":"resource" } } }, "thumbnail":{ "width":60, "height":21, "length":3125, "lastModified":1395331380000, "mimeType":"image/png", "_links":{ "site":{ "href":"http://localhost:8080/site/binaries/site/binaries/thumbnail/content/gallery/hap/banners/banner-1.png", "type":"resource" } } } }, "u9a3f1f5c530243c49bec584e810ffa2f":{ "id":"9a3f1f5c-5302-43c4-9bec-584e810ffa2f", "_links":{ "site":{ "href":"/site/myapp", "type":"internal" } }, "name":"banner2", "displayName":"banner2", "content":{ "name":"hap:content", "displayName":"hap:content", "value":"\n \n\n <p>Banner description</p>\n\n \n " }, "title":"Sample banner 2", "image":{ "$ref":"/content/udb5907cce507460eb54dde5d5c784e0a" }, "address":false, "localeString":"en" }, "udb5907cce507460eb54dde5d5c784e0a":{ "id":"db5907cc-e507-460e-b54d-de5d5c784e0a", "_links":{ "site":{ "href":"http://localhost:8080/site/binaries/site/binaries/content/gallery/hap/banners/banner2.png", "type":"resource" } }, "name":"banner2.png", "displayName":"banner2.png", "fileName":"banner2.png", "description":"Description for banner2.png", "original":{ "width":700, "height":250, "length":103656, "lastModified":1395504540000, "mimeType":"image/png", "_links":{ "site":{ "href":"http://localhost:8080/site/binaries/site/binaries/content/gallery/hap/banners/banner2.png", "type":"resource" } } }, "thumbnail":{ "width":60, "height":21, "length":2213, "lastModified":1395504540000, "mimeType":"image/png", "_links":{ "site":{ "href":"http://localhost:8080/site/binaries/site/binaries/thumbnail/content/gallery/hap/banners/banner2.png", "type":"resource" } } } } } }
Note that the page field always has a components JSON Array and each item in the components JSON Array may contain its child components JSON Array recursively. This makes sense because an HST page consists of a hiearchical collection of descendant components. See HST Component Configuration for detail.
If a component contributes a document, then the document content item model will be included as a reference in models field inside the component representation like the following fragment shown above:
{ "id":"r19_r1_r1_r1", "name":"banner", "componentClass":"org.onehippo.cms7.essentials.components.EssentialsDocumentComponent", "type":"CONTAINER_ITEM_COMPONENT", "label":"Banner", "models":{ "document":{ "$ref":"/content/u895fb1b6410d497298946b6a06d2b361" } }, "_meta":{ "paramsInfo":{ "document":"banners/banner1" }, "params":{ "com.example.cms7.targeting.TargetingParameterUtil.hide":"off", "document":"banners/banner1", "org.hippoecm.hst.core.component.template":"webfile:/freemarker/hstdefault/essentials-banner.ftl" } }, "_links":{ "componentRendering":{ "href":"/site/myapp/resourceapi?_hn:type=component-rendering&_hn:ref=r19_r1_r1_r1" } } }
Note that the real content representation of the contributed document content item model is not included directly inside the models field. Instead it contains only a JSON Pointer referece as JSON String ("$ref":"/content/u895fb1b6410d497298946b6a06d2b361"), by which you can find the real content item representation under the content top level field by the key. Also, a component representation may include metadata, links and other model representations such as menu.
Content item model representations are always included in the content top level field for effectiveness (e.g, to avoid multiple serializations for the same content item) like the following:
"content": { "u895fb1b6410d497298946b6a06d2b361":{ "id":"895fb1b6-410d-4972-9894-6b6a06d2b361", "_links":{ "site":{ "href":"/site/myapp", "type":"internal" } }, "name":"banner1", "displayName":"banner1", "content":{ "name":"hap:content", "displayName":"hap:content", "value":"\n \n\n <p>Banner description</p>\n\n \n " }, "title":"Sample banner", "image":{ "$ref":"/content/ub89d576f680a4bbf9c272dced9da3d6c" }, "address":false, "localeString":"en" }, "ub89d576f680a4bbf9c272dced9da3d6c":{ "id":"b89d576f-680a-4bbf-9c27-2dced9da3d6c", "_links":{ "site":{ "href":"http://localhost:8080/site/binaries/site/binaries/content/gallery/hap/banners/banner-1.png", "type":"resource" } }, "name":"banner-1.png", "displayName":"banner-1.png", "fileName":"banner-1.png", "description":"Description for banner-1.png", "original":{ "width":700, "height":250, "length":151972, "lastModified":1395331380000, "mimeType":"image/png", "_links":{ "site":{ "href":"http://localhost:8080/site/binaries/site/binaries/content/gallery/hap/banners/banner-1.png", "type":"resource" } } }, "thumbnail":{ "width":60, "height":21, "length":3125, "lastModified":1395331380000, "mimeType":"image/png", "_links":{ "site":{ "href":"http://localhost:8080/site/binaries/site/binaries/thumbnail/content/gallery/hap/banners/banner-1.png", "type":"resource" } } } }, // ... }
Each content item representation also has links and other fields (such as title, content and address) which are extracted from its mapped HST Content Bean class.
Also note that the referenced content item, the image field in the above example, contains only a JSON Pointer reference as JSON String as well ("$ref":"/content/ub89d576f680a4bbf9c272dced9da3d6c"), so you can find the image content item representation separately.
Maximum Content Item Reference Depth Level
As explained above, if an HST Content Bean object is referred by an HstComponent, the content item will be included only once in the top level content field. In this case, the reference depth level is 1, meaning that the content item is in the first reference depth level from the HstComponent.
Now, suppose the specific content item also has a reference to another content item, which makes the second referenced content item be in the reference depth level 2 from the HstComponent. Any referenced content item in the second or any deeper level will not be included in the top level content field, by default, unless another HstComponent has a direct reference to it. So, the first content item representation will include only a JSON Pointer reference to the second content item. The maximum content item reference depth level is set to 1 in the system, by default.
How to Include up to N Level Content Items?
You can change the maximum content item reference depth level in two ways: (a) changing it per request, (b) changing the system default setting. (a) takes the precedence over (b).
In order to change it per request, add _maxreflevel request parameter like the following:
- http://localhost:8080/site/myapp/resourceapi/news/?_maxreflevel=2
The response will include referred content items in up to depth level 2.
In order to change the default maximum reference depth level, add the following in the HST-2 Container Configuration file (e.g, ${catalina.base}/conf/hst.properties):
# The default maximum content item reference depth level #pagemodelapi.v09.defaultMaxContentReferenceLevel = 1 pagemodelapi.v09.defaultMaxContentReferenceLevel = 3
The above configuration will change it from 1 to 3, which makes the response include content items in up to reference depth level 3.
Links: Internal vs. External
In the content section, the _links object may contain a site link which specifies a type. The type can be either "internal" or "external". "internal" means the page can be represented within the current SPA. This means the SPA can use an XHR request and do a partial page update instead of making a full page request. In the above example, we have:
"_links":{ "site":{ "href":"/site/myapp", "type":"internal" } },
It means that the link, /site/myapp, can be represented within the current SPA. The SPA may opt to make an XHR call, at /resourceapi/site/myapp for example, instead of making a full page request.
Rich Content Rewriting
The top level content field, containing pairs of JSON Identifier key and content item representation in JSON, are serialized versions of HST Content Beans. The HippoDocumentBean instances may contain rich text fields, which are represented by HippoHtmlBean objects. When HippoHtmlBean objects are serialized to a JSON, the HTML content of the HippoHtmlBean objects are rewritten, to make sure that images and links are accessible and usable from the SPA. For example when a WCMS document contains a link to another document in a rich text field, the link is rewritten to more accessible and usable markups. See the following example:
<a href="/news/news1.html" data-type="internal"/>
The SPA retrieves the href attribute which it can directly navigate to or it can do something smarter with it optionally: If the data-type is "internal", then the link can be used as an XHR call (typically something like /resourceapi/news/news1.html) which updates part of the pages, instead of making a new page request. If, however, the data-type is "external", then the SPA should always do a full page request (never an XHR call) because the linked page cannot be displayed within the current SPA. Typically this happens when:
- There is a hybrid setup with SPA pages and the link represents a normal server side rendering page.
- The link belongs to a document that is part of a different channel which is not part of the SPA.
Summary
The built-in Delivery API provides an intuitive REST API endpoint for SPAs, ensuring seamless integration with WCMS. SPAs may consume the JSON API responses to construct pages, components, menus, etc. with referenced content item models or domain-specific models, contributed by each component and aggregated for the page, in their own SPA frameworks. The Delivery API provides a generic, extensible, and most effective aggregated model representation for easier SPA development support.