Friday, August 7, 2020

Sling Feature Flags for continuous integration in Adobe Experience Manager(AEM)| Feature Toggles in AEM

Sling Feature Flags for continuous integration in Adobe Experience Manager(AEM)| Feature Toggles in AEM


This tutorial explains the details of Sling Feature Flags in AEM.

Feature Flags

Feature Flags are used to select whether a particular feature is enabled or not. This allows us to continuously deploy new features of an application without making them globally available yet.

apache-sling-feature-flags

Feature flag support consists of two parts:

The feature flag itself represented by the Feature interface and the application providing a feature guarded by a feature flag.

A feature flag is simply a boolean condition that modifies the behavior of a component, module, or function in your application.

Features may be enabled based on various contextual data:

  • Time of Day
  • Segmentation Data (gender, age, etc.), if available
  • Request Parameter
  • Request Header
  • Cookie Value
  • Static Configuration

Configure Feature Flags

Feature flags can be provided by registering org.apache.sling.featureflags.Feature services. Alternatively feature flags can be provided by factory configuration with factory PID org.apache.sling.featureflags.impl.ConfiguredFeature

Enable through Feature service

Enable a Feature service as shown below, the feature service should implement org.apache.sling.featureflags.Feature. Add the logic to enable/disable the feature into the isEnabled(ExecutionContext ec) method, the method returns boolean true(feature enabled) or false(feature disabled) based on the implementation logic. The ExecutionContext interface provides access to the context for evaluating whether a feature is enabled or not. Enable a name and description for the feature, this will help us to identify the features through feature console.

import org.apache.sling.featureflags.Feature;@Component(service = Feature.class, immediate = true)
public class SampleFeature implements Feature {
@Override
public String getDescription() {
return "Sample Feature";
}
@Override
public String getName() {
return "sample-feature";
}
@Override
public boolean isEnabled(ExecutionContext ec) {

// Write a logic to evaluate the feature flag to true or false

return false;

}

}

This is a dynamic configuration, the logic is evaluated to identify the flag value.

As an example I am creating a feature — CalculateTotal, based on the request parameter(newTotal=true/false) the corresponding calculation logic is enabled, implement the required logic to enable or disable the features.

import org.apache.sling.featureflags.ExecutionContext;
import org.apache.sling.featureflags.Feature;
import org.osgi.service.component.annotations.Component;
@Component(service = Feature.class, immediate = true)
public class CalculateNewTotalFeature implements Feature {

@Override
public String getDescription() {
return "Calculate New Total Feature";
}
@Override
public String getName() {
return "CalculateNewTotalFeature";
}
@Override
public boolean isEnabled(ExecutionContext ec) {
// Feature Flag to enable new total calculation based on the request parameter
if(ec.getRequest()!=null)
{
return Boolean.valueOf(ec.getRequest().getParameter("newTotal"));
}

return false;
}
}

Sample Servlet guarded by the feature flag

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.apache.sling.featureflags.Features;
import org.osgi.framework.Constants;
import org.apache.sling.api.servlets.HttpConstants;
@Component(service=Servlet.class,immediate = true,
property={
Constants.SERVICE_DESCRIPTION + "=Simple Demo Servlet",
"sling.servlet.methods=" + HttpConstants.METHOD_GET,
"sling.servlet.paths="+ "/services/calculateTotalServlet",
})
public class CalculateTotalServlet extends SlingSafeMethodsServlet {

@Reference
private Features features;
private static final long serialVersionUID = 1L;@Override
protected void doGet(final SlingHttpServletRequest req,
final SlingHttpServletResponse resp) throws ServletException, IOException {
int total=0;
if(features.isEnabled("CalculateNewTotalFeature"))
{
total=Integer.valueOf(req.getParameter("qty"))*Integer.valueOf(req.getParameter("val"))*Integer.valueOf(req.getParameter("tax"));
}else
{
total=Integer.valueOf(req.getParameter("qty"))*Integer.valueOf(req.getParameter("val"));
}
resp.setContentType("text/plain");
resp.getWriter().write("Total = " +total) ;
}
}

Different calculation logic is used based on the newTotal flag value in the request parameter

http://localhost:4502/services/calculateTotalServlet?qty=2&val=3&tax=10&newTotal=true — new logic is used and the outcome is 60

http://localhost:4502/services/calculateTotalServlet?qty=2&val=3&tax=10&newTotal=false — Old logic is used and the outcome is 6

Enable through factory PID org.apache.sling.featureflags.Feature

The feature can also be enabled through factory PID org.apache.sling.featureflags.impl.ConfiguredFeature (Apache Sling Configured Feature)

Enable the run mode-specific configuration — org.apache.sling.featureflags.impl.ConfiguredFeature-sample-feature.xml

Sample configure details are

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
xmlns:jcr="http://www.jcp.org/jcr/1.0" jcr:primaryType="sling:OsgiConfig"
name="sample-feature"
description="Sample Feature"
enabled="{Boolean}true"/>
apache-sling-feature-flags

name — Short name of this feature. This name is used to refer to this feature when checking for it to be enabled or not. This property is required and defaults to a name derived from the feature’s class name and object identity. It is strongly recommended to define a useful and unique for the feature

description — Description of the feature. The intent is to describe the behavior of the application if this feature would be enabled. It is recommended to define this property. The default value is the value of the name property.

enabled — Boolean flag indicating whether the feature is enabled or not by this configuration

This is going to be a static configuration, the flag value is set in the configuration. The static features flags can be used in the application as same as the dynamic feature flags to guard the features.

The configurations can be created/modified(not recommended) and verified through OSGI Console

Image for post
apache-sling-feature-flags

The registered feature flags(default and custom) status can be viewed from OSGI console — http://localhost:4502/system/console/features(displays both dynamic and static flags)

apache-sling-feature-flags

In my case, the feature flag(sample-feature) is disabled by default as the logic is based on the request parameter, enabled when the request parameter contains “newTotal=true”

The static flag — sample-feature

Image for post

Feature Flags for Render Condition in Granite UI Fields

Render condition is a mechanic to indicate if the component should be rendered or not. A typical example would be to render a submit button based on whether the current user has a privilege to create or modify a resource.

The actual decision making logic itself is pluggable, where a rendercondition component can be created as needed.

/libs/granite/ui/components/coral/foundation/renderconditions/feature — A condition that decides based on Sling’s Feature Flag. The UI fields can be enabled or disabled based on the Feature Flag configuration.

I want to display the “Additional Comments” dialog field based on the feature flag -”sample-feature”, display the filed if the feature flag is enabled, and hide if disabled.

Let us now enable the render condition for the specific dialog field

Create a node of type “nt:unstructured” with the name “granite:rendercondition” under the dialog field

Add the following properties to the “granite:rendercondition” node

“sling:resourceType” — granite/ui/components/coral/foundation/renderconditions/feature

feature- feature name(sample-feature), the feature can be of the type dynamic or static type.

apache-sling-feature-flags-aem

The dialog field “Additional Comments” is displayed based on the status(enabled or disabled) of the “simple-feature” flag.

The field is displayed when the flag is enabled

apache-sling-feature-flags
apache-sling-feature-flags

The field is hidden when the flag is disabled

apache-sling-feature-flags
apache-sling-feature-flags

Feature Flags in Workflow Launcher

The feature flags can be used to trigger the workflow launchers along with the other conditions.

Features — Add the list of features that must be enabled to trigger the launcher

Disabled Features — Add the list of features that must be disabled to trigger the launcher.

apache-sling-feature-flags

The sling feature flags can be used in AEM to manage(enable/disable) the features externally from the application code, the flags can be dynamic based on the implementation logic or static. The features can be enabled based on different context data e.g request parameters, cookies, resource properties, etc. The feature flags help us to continuously deliver features without impacting the end-users also helps to dry run the new features without enabling to public users.

References

https://sling.apache.org/documentation/the-sling-engine/featureflags.html

https://helpx.adobe.com/experience-manager/6-5/sites/developing/using/reference-materials/granite-ui/api/jcr_root/libs/granite/ui/components/coral/foundation/renderconditions/feature/index.html

https://helpx.adobe.com/experience-manager/6-5/sites/developing/using/reference-materials/javadoc/org/apache/sling/featureflags/package-summary.html



Tuesday, August 4, 2020

How to Enable Proximity Search in Adobe Search and Promote | Location-aware search with Adobe Search and Promote

How to Enable Proximity Search in Adobe Search and Promote | Location-aware search with Adobe Search and Promote


This tutorial explains the details on enabling Proximity Search in Adobe Search and Promote.

Proximity Search

Proximity Search lets you associate a unique location with any page on your website, and then search and sort results by proximity (distance) from a given location.

For example, suppose you have populated pages on your website with United States postal ZIP code metadata such as the following:

<meta name="zipcode" content="84057">

After indexing your site, you perform the following search:

...&sp_q_location_1=84057&sp_x_1=zip&sp_q_max_1=100&sp_s=zip_proximity

The result set contains any documents located within 100 miles of ZIP code 84057, sorted in ascending order of distance from this ZIP code.

The telephone area codes also can be used for United States locations. Or, you can use latitude/longitude pairs to specify locations in your site metadata and in your search criteria. The location type is automatically determined from the form of the data that is provided.

Three-digit location values (“DDD”, where each “D” represents a decimal digit from 0–9) are treated as a United States telephone area code.

Five or five-dash-four digit location values (“DDDDD” or “DDDDD-DDDD”) are treated as a United States postal ZIP code.

Location values in the exact form of “±DD.DDDD±DDD.DDDD” are treated as a latitude/longitude pair. The first signed numeric value specifies latitude and the second signed numeric value represents longitude.

If you specify a positive latitude value, or positive longitude value, or both, the “+” character in the URL must be encoded as %2b. Otherwise, the “+” is interpreted as space, and the value is not recognized as a valid location.

When you search by proximity there is a special “proximity output field” created for that search. The field is populated with the relative distance between the location that is specified in the search criteria, and the location that is associated with each search result. This special field is named for the location-type field that is used in the search criteria with “_proximity” added to the end.

In the example search above, the results are sorted in ascending order of “zip_proximity.” That is the distance between the specified ZIP code (84057) and each result’s “zip” field location. You can also use this special “proximity output field” to display the relative distance for each search result, in either kilo meters or miles, using the Search template tag.

Configure Proximity Search

Let us now enable the proximity search in Adobe Search and Promote, I am going to use the IndexConnector to index the documents. Refer to the below URL for details on enabling IndexConnector and custom presentation and transport templates.

Proximity Search with zip code

Let us now enable the proximity search with zip proximity, I am going to index the below sample feed data through Index Connector

<feed xmlns:xs="http://www.w3.org/2001/XMLSchema" version="2.0">
<channel>
<title>Product Feed</title>
<Item>
<link>https://qa.example.com/product-title/p/prod1</link>
<title>
<![CDATA[Java Title1]]>
</title>
<description>
<![CDATA[<p>Prod1 description</p>]]>
</description>
<productType>Java</productType>
<ProductId>prod1</ProductId>
<zipcode>55123</zipcode>
<imageUrl>/content/dam/Images/product/prod1.jpg</imageUrl>
</Item>
<Item>
<link>https://qa.example.com/product-title/p/prod2</link>
<title>
<![CDATA[Java Title2]]>
</title>
<description>
<![CDATA[<p>Prod2 description</p>]]>
</description>
<productType>java</productType>
<ProductId>prod2</ProductId>
<zipcode>92307</zipcode>
<imageUrl>/content/dam/Images/product/prod2.jpg</imageUrl>
</Item>
<Item>
<link>https://qa.example.com/product-title/p/prod3</link>
<title>
<![CDATA[Java Title3]]>
</title>
<description>
<![CDATA[<p>Prod3 description</p>]]>
</description>
<productType>java</productType>
<ProductId>prod3</ProductId>
<zipcode>55103</zipcode>
<imageUrl>/content/dam/Images/product/prod3.jpg</imageUrl>
</Item>
<Item>
<link>https://qa.example.com/product-title/p/prod5</link>
<title>
<![CDATA[Lava Title]]>
</title>
<description>
<![CDATA[<p>Lava description</p>]]>
</description>
<productType>Lava</productType>
<zipcode>55109</zipcode>
<ProductId>prod5</ProductId>
<imageUrl>/content/dam/Images/product/prod5.jpg</imageUrl>
</Item>
</channel>
</feed>

Enable the metadata configuration for new zipcode field — Settings →Metadata →Definitions

proximity-search-in-adobe-search-and-promote

Configure the new field in IndexConnector — Settings →Crawling —>Index Connector

proximity-search-in-adobe-search-and-promote

Modify the presentation and Transports templates to include the “proximity output field" and if required metadata field zip code to the response, sample custom presentation & transports templates below (modify the corresponding templates used)

custom_backend_json.tpl (Transport)

<search-content-type-header charset="UTF-8">
{
"general": {
"query" : "<search-query />",
"total" : "<search-total />",
"lower" : "<search-lower />",
"upper" : "<search-upper />"
},

"facets" : [
{
"name" : "n1",
"values" : [<search-field-value-list name="n1" quotes="yes" data="values" sortby="values" encoding="json" />],
"counts" : [<search-field-value-list name="n1" quotes="no" data="results" sortby="values" />]
}
],
"results" : [
<search-results>
{
"fields" :
[
{
"name" : "mdi",
"value" : "<search-display-field name="mdi" length="500" encoding="json" />"
},
{
"name" : "title",
"value" : "<search-display-field name="title" encoding="json" />"
},
{
"name" : "productType",
"value" : "<search-display-field name="productType" encoding="json" />"
},
{
"name" : "zip",
"value" : "<search-display-field name="zip" encoding="json" />"
},
{
"name" : "relative_distance",
"value" : "<search-display-field name="zip_proximity" encoding="json" />"
}

]
}
<search-if-not-last>,</search-if-not-last>
</search-results>
]
}

custom_presentation_json.tmpl (Presentation)

<guided-content-type-header content="application/json" />
<guided-if-query-param-defined gsname="callback" /><guided-query-param gsname="callback" />(</guided-if-query-param-defined>
{
"general" :
{
"query" : "<guided-query-param gsname='q' />",
"total" : "<guided-results-total />",
"page_lower" : "<guided-results-lower>",
"page_upper" : "<guided-results-upper>",
"page_total": "<guided-page-total/>"
},
"facets" :
[

],
"results" :
[
<guided-results gsname="default">
{
"index" : "<guided-result-index />",
"title" : "<guided-result-field gsname="title" escape="ijson" />",
"productType" : "<guided-result-field gsname="productType" escape="ijson" />",
"zip" : "<guided-result-field gsname="zip" escape="ijson" />",
"relative_distance" : "<guided-result-field gsname="relative_distance" escape="ijson" />"

}<guided-if-not-last>,</guided-if-not-last>
</guided-results>
]
}
<guided-if-query-param-defined gsname="callback">)</guided-if-query-param-defined>

Run the Full Index now and execute the query by specifying the distance

http://xxxxxxxx.guided.ss-omtrdc.net/?do=json&sp_q_location_1=55123&sp_x_1=zip&sp_q_max_1=10&sp_s=zip_proximity

The result contains any documents located within 10 miles of ZIP code 55123, sorted in ascending order of distance from this ZIP code.

{
"general": {
"query": "",
"total": "1",
"page_lower": "1",
"page_upper": "1",
"page_total": "1"
},
"facets": [],
"results": [
{
"index": "",
"title": "Java Title1",
"productType": "Java",
"zip": "55123",
"relative_distance": "0.00"

}
]
}

http://xxxxxxxx.guided.ss-omtrdc.net/?do=json&sp_q_location_1=55123&sp_x_1=zip&sp_q_max_1=20&sp_s=zip_proximity

The result contains any documents located within 20 miles of ZIP code 55123, sorted in ascending order of distance from this ZIP code.

{
"general": {
"query": "",
"total": "3",
"page_lower": "1",
"page_upper": "3",
"page_total": "1"
},
"facets": [],
"results": [
{
"index": "",
"title": "Java Title1",
"productType": "Java",
"zip": "55123",
"relative_distance": "0.00"

},
{
"index": "",
"title": "Java Title3",
"productType": "java",
"zip": "55103",
"relative_distance": "10.98"

},
{
"index": "",
"title": "Lava Title",
"productType": "Lava",
"zip": "55109",
"relative_distance": "15.51"

}
]
}

http://xxxxxxxx.guided.ss-omtrdc.net/?do=json&sp_q_location_1=55123&sp_x_1=zip&sp_q_max_1=1500&sp_s=zip_proximity

The result contains any documents located within 1500 miles of ZIP code 55123, sorted in ascending order of distance from this ZIP code.

{
"general": {
"query": "",
"total": "4",
"page_lower": "1",
"page_upper": "4",
"page_total": "1"
},
"facets": [],
"results": [
{
"index": "",
"title": "Java Title1",
"productType": "Java",
"zip": "55123",
"relative_distance": "0.00"

},
{
"index": "",
"title": "Java Title3",
"productType": "java",
"zip": "55103",
"relative_distance": "10.98"

},
{
"index": "",
"title": "Lava Title",
"productType": "Lava",
"zip": "55109",
"relative_distance": "15.51"

},
{
"index": "",
"title": "Java Title2",
"productType": "java",
"zip": "92307",
"relative_distance": "1448.84"

}
]
}

Proximity Search with telephone area code

The proximity search can also be enabled through 3 digit telephone area code.

Sample Feed data

<feed xmlns:xs="http://www.w3.org/2001/XMLSchema" version="2.0">
<channel>
<title>Product Feed</title>
<Item>
<link>https://qa.example.com/product-title/p/prod1</link>
<title>
<![CDATA[Java Title1]]>
</title>
<description>
<![CDATA[<p>Prod1 description</p>]]>
</description>
<productType>Java</productType>
<ProductId>prod1</ProductId>
<zipcode>55123</zipcode>
<areacode>218</areacode>
<imageUrl>/content/dam/Images/product/prod1.jpg</imageUrl>
</Item>
<Item>
<link>https://qa.example.com/product-title/p/prod2</link>
<title>
<![CDATA[Java Title2]]>
</title>
<description>
<![CDATA[<p>Prod2 description</p>]]>
</description>
<productType>java</productType>
<ProductId>prod2</ProductId>
<zipcode>92307</zipcode>
<areacode>320</areacode>
<imageUrl>/content/dam/Images/product/prod2.jpg</imageUrl>
</Item>
<Item>
<link>https://qa.example.com/product-title/p/prod3</link>
<title>
<![CDATA[Java Title3]]>
</title>
<description>
<![CDATA[<p>Prod3 description</p>]]>
</description>
<productType>java</productType>
<ProductId>prod3</ProductId>
<zipcode>55103</zipcode>
<areacode>507</areacode>
<imageUrl>/content/dam/Images/product/prod3.jpg</imageUrl>
</Item>
<Item>
<link>https://qa.example.com/product-title/p/prod5</link>
<title>
<![CDATA[Lava Title]]>
</title>
<description>
<![CDATA[<p>Lava description</p>]]>
</description>
<productType>Lava</productType>
<zipcode>55109</zipcode>
<areacode>612 </areacode>
<ProductId>prod5</ProductId>
<imageUrl>/content/dam/Images/product/prod5.jpg</imageUrl>
</Item>
</channel>
</feed>

Enable the metadata configuration for new areacode field — Settings →Metadata →Definitions

proximity-search-in-adobe-search-and-promote

Configure the new field in IndexConnector — Settings →Crawling →Index Connector

proximity-search-in-adobe-search-and-promote

Modify the presentation and Transports templates to include the “proximity output field” and if required metadata field area code to the response, sample custom presentation & transports templates below (modify the corresponding templates used)

custom_backend_json.tpl (Transport)

<search-content-type-header charset="UTF-8">
{
"general": {
"query" : "<search-query />",
"total" : "<search-total />",
"lower" : "<search-lower />",
"upper" : "<search-upper />"
},

"facets" : [
{
"name" : "n1",
"values" : [<search-field-value-list name="n1" quotes="yes" data="values" sortby="values" encoding="json" />],
"counts" : [<search-field-value-list name="n1" quotes="no" data="results" sortby="values" />]
}
],
"results" : [
<search-results>
{
"fields" :
[
{
"name" : "mdi",
"value" : "<search-display-field name="mdi" length="500" encoding="json" />"
},
{
"name" : "title",
"value" : "<search-display-field name="title" encoding="json" />"
},
{
"name" : "productType",
"value" : "<search-display-field name="productType" encoding="json" />"
},
{
"name" : "zip",
"value" : "<search-display-field name="zip" encoding="json" />"
},
{
"name" : "relative_distance",
"value" : "<search-display-field name="zip_proximity" encoding="json" />"
},
{
"name" : "areacode",
"value" : "<search-display-field name="areacode" encoding="json" />"
},
{
"name" : "relative_distance_areacode",
"value" : "<search-display-field name="areacode_proximity" encoding="json" />"
}

]
}
<search-if-not-last>,</search-if-not-last>
</search-results>
]
}

custom_presentation_json.tmpl (Presentation)

<guided-content-type-header content="application/json" />
<guided-if-query-param-defined gsname="callback" /><guided-query-param gsname="callback" />(</guided-if-query-param-defined>
{
"general" :
{
"query" : "<guided-query-param gsname='q' />",
"total" : "<guided-results-total />",
"page_lower" : "<guided-results-lower>",
"page_upper" : "<guided-results-upper>",
"page_total": "<guided-page-total/>"
},
"facets" :
[

],
"results" :
[
<guided-results gsname="default">
{
"index" : "<guided-result-index />",
"title" : "<guided-result-field gsname="title" escape="ijson" />",
"productType" : "<guided-result-field gsname="productType" escape="ijson" />",
"zip" : "<guided-result-field gsname="zip" escape="ijson" />",
"relative_distance" : "<guided-result-field gsname="relative_distance" escape="ijson" />",
"areacode" : "<guided-result-field gsname="areacode" escape="ijson" />",
"relative_distance_areacode" : "<guided-result-field gsname="relative_distance_areacode" escape="ijson" />"
}<guided-if-not-last>,</guided-if-not-last>
</guided-results>
]
}
<guided-if-query-param-defined gsname="callback">)</guided-if-query-param-defined>

Run the Full Index now and execute the query by specifying the distance

http://xxxxxx.guided.ss-omtrdc.net/?do=json&sp_q_location_1=218&sp_x_1=areacode&sp_q_max_1=200&sp_s=areacode_proximity

The result contains any documents located within 200 miles of area code 218, sorted in ascending order of distance from this area code.

{
"general": {
"query": "",
"total": "3",
"page_lower": "1",
"page_upper": "3",
"page_total": "1"
},
"facets": [],
"results": [
{
"index": "",
"title": "Java Title1",
"productType": "Java",
"zip": "55123",
"relative_distance": "",
"areacode": "218",
"relative_distance_areacode": "0.00"

},
{
"index": "",
"title": "Java Title2",
"productType": "java",
"zip": "92307",
"relative_distance": "",
"areacode": "320",
"relative_distance_areacode": "125.02"

},
{
"index": "",
"title": "Lava Title",
"productType": "Lava",
"zip": "55109",
"relative_distance": "",
"areacode": "612",
"relative_distance_areacode": "171.86"

}
]
}

The search can also be enabled based on the latitude/longitude by enabling metadata with latitude/longitude value in “±DD.DDDD±DDD.DDDD” format and searching with the same value(e.g. +44.8041, -93.1668).

The proximity search will associate the location data to the search records and allows us to search the records based on the relative location of the records.

References