How to add flexible search restriction for Product with custom session attribute and resolve all exceptions

OOTB hybris has a powerful “Flexible Search Restriction” system, which allows transparently restrict access to any hybris item type. Due to flexible search restrictions are implemented on a very low level of hybris ORM time to time unexpected issues can appear even for simple restrictions.

For example simple task: show products for users only in case if product and users root B2BUnit have relation to same CxSegment.

To implement it you need:

  1. Create relation between CxSegment and B2BUnit/Product:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
        <relation code="CxSegmentToProductRelation" autocreate="true" generate="true" localized="false">
            <deployment table="CxSegToProdRel" typecode="10310"/>
            <sourceElement qualifier="cxSegments" type="CxSegment" cardinality="many"/>
            <targetElement qualifier="products" type="Product" cardinality="many" collectiontype="set"/>
        </relation>
        <relation code="CxSegmentToB2BUnitRelation" autocreate="true" generate="true" localized="false">
            <deployment table="CxSegToUnitRel" typecode="10311"/>
            <sourceElement qualifier="cxSegments" type="CxSegment" cardinality="many"/>
            <targetElement qualifier="b2bUnits" type="B2BUnit" cardinality="many" collectiontype="set"/>
        </relation>
  1. Create flexible search restriction with query:
1
EXISTS({{select {stp.pk} from {CxSegmentToProductRelation as stp JOIN CxSegment as s ON {stp.source} = {s.pk}} where {s.code} IN (?session.userSegments) AND {stp.target} = {item.pk}}})

If session custom attribute (?session.userSegments) will be not populated before execution of flexible search query on ProductModel than flexible search will fail with error:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[2018/12/21 16:05:48.133] ERROR [hybrisHTTP36] [FlexibleSearch] Flexible search error occured...
Dec 21, 2018 4:05:48 PM org.apache.catalina.core.ApplicationDispatcher invoke
SEVERE: Servlet.service() for servlet [DispatcherServlet] threw exception
de.hybris.platform.jalo.flexiblesearch.FlexibleSearchException: could not translate value expression 'session.userSegments'[HY-0]
	at de.hybris.platform.jalo.flexiblesearch.SessionParamTranslator.translatePathValueKeys(SessionParamTranslator.java:91)
	at de.hybris.platform.jalo.flexiblesearch.FlexibleSearch.translatePathValueKeys(FlexibleSearch.java:2021)
	at de.hybris.platform.jalo.flexiblesearch.FlexibleSearch.search(FlexibleSearch.java:1426)
	at de.hybris.platform.jalo.flexiblesearch.FlexibleSearch.search(FlexibleSearch.java:1380)
	at de.hybris.platform.jalo.link.LinkManager.getLinkedItems(LinkManager.java:520)
	at de.hybris.platform.jalo.Item.getLinkedItems(Item.java:2513)
	at de.hybris.platform.cms2lib.components.GeneratedProductCarouselComponent.getProducts(GeneratedProductCarouselComponent.java:247)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at de.hybris.platform.jalo.type.ReflectionAttributeAccess$AttributeMethod.invokeGetter(ReflectionAttributeAccess.java:739)
	at de.hybris.platform.jalo.type.ReflectionAttributeAccess.getValue(ReflectionAttributeAccess.java:944)
	at de.hybris.platform.jalo.Item.getAttribute(Item.java:1863)
	at de.hybris.platform.servicelayer.internal.model.impl.JaloPersistenceObject.readRawValue(JaloPersistenceObject.java:111)
	at de.hybris.platform.servicelayer.internal.converter.impl.ItemModelConverter.readSingleAttribute(ItemModelConverter.java:1381)
	at de.hybris.platform.servicelayer.internal.converter.impl.ItemAttributeProvider.getAttribute(ItemAttributeProvider.java:108)
	at de.hybris.platform.servicelayer.model.ItemModelContextImpl.loadUnlocalizedAttribute(ItemModelContextImpl.java:292)
	at de.hybris.platform.servicelayer.model.ItemModelContextImpl.getValue(ItemModelContextImpl.java:252)
	at de.hybris.platform.servicelayer.model.ItemModelContextImpl.getPropertyValue(ItemModelContextImpl.java:268)
	at de.hybris.platform.cms2lib.model.components.ProductCarouselComponentModel.getProducts(ProductCarouselComponentModel.java:183)
	at de.hybris.platform.acceleratorfacades.productcarousel.impl.DefaultProductCarouselFacade.fetchProductsForNonPreviewMode(DefaultProductCarouselFacade.java:90)
	at de.hybris.platform.acceleratorfacades.productcarousel.impl.DefaultProductCarouselFacade.collectProducts(DefaultProductCarouselFacade.java:69)

Unfortunately adding population of custom session attribute somewhere in storefront (for example, after login or in BeforeControllerHandler) will resolve exception above, but there would be still issue with exception generated by solr indexer job, which is executed in local view context and doesn’t have custom session attribute:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[2018/12/21 16:15:36.354] ERROR [update-blogIndex-cronJob::de.hybris.platform.servicelayer.internal.jalo.ServicelayerJob] (update-blogIndex-cronJob) [FlexibleSearch] Flexible search error occured...
[2018/12/21 16:15:36.390] ERROR [update-blogIndex-cronJob::de.hybris.platform.servicelayer.internal.jalo.ServicelayerJob] (update-blogIndex-cronJob) [Job] Caught throwable could not translate value expression 'session.userSegmentsa'
de.hybris.platform.servicelayer.search.exceptions.FlexibleSearchException: could not translate value expression 'session.userSegmentsa'
	at de.hybris.platform.servicelayer.search.impl.DefaultFlexibleSearchService$2.execute(DefaultFlexibleSearchService.java:422)
	at de.hybris.platform.servicelayer.search.impl.DefaultFlexibleSearchService$2.execute(DefaultFlexibleSearchService.java:1)
	at de.hybris.platform.servicelayer.session.impl.DefaultSessionService.executeInLocalView(DefaultSessionService.java:89)
	at de.hybris.platform.servicelayer.search.impl.DefaultFlexibleSearchService.getJaloResult(DefaultFlexibleSearchService.java:396)
	at de.hybris.platform.servicelayer.search.impl.DefaultFlexibleSearchService.search(DefaultFlexibleSearchService.java:168)
	at de.hybris.platform.solrfacetsearch.indexer.impl.DefaultIndexerQueriesExecutor.getPks(DefaultIndexerQueriesExecutor.java:87)
	at de.hybris.platform.solrfacetsearch.indexer.strategies.impl.AbstractIndexerStrategy.executeIndexerQuery(AbstractIndexerStrategy.java:244)
	at de.hybris.platform.solrfacetsearch.indexer.strategies.impl.AbstractIndexerStrategy.resolvePks(AbstractIndexerStrategy.java:336)
	at de.hybris.platform.solrfacetsearch.indexer.strategies.impl.AbstractIndexerStrategy.doExecute(AbstractIndexerStrategy.java:158)
	at de.hybris.platform.solrfacetsearch.indexer.strategies.impl.AbstractIndexerStrategy.execute(AbstractIndexerStrategy.java:124)
	at de.hybris.platform.solrfacetsearch.indexer.impl.DefaultIndexerService.updateIndex(DefaultIndexerService.java:90)
	at de.hybris.platform.solrfacetsearch.indexer.cron.SolrIndexerJob.indexItems(SolrIndexerJob.java:81)
	at de.hybris.platform.solrfacetsearch.indexer.cron.SolrIndexerJob.performIndexingJob(SolrIndexerJob.java:57)
	at de.hybris.platform.solrfacetsearch.indexer.cron.AbstractIndexerJob.perform(AbstractIndexerJob.java:40)
	at de.hybris.platform.servicelayer.internal.jalo.ServicelayerJob.performCronJob(ServicelayerJob.java:38)
	at de.hybris.platform.cronjob.jalo.Job.execute(Job.java:1390)
	at de.hybris.platform.cronjob.jalo.Job.performImpl(Job.java:814)
	at de.hybris.platform.cronjob.jalo.Job.access$1(Job.java:767)
	at de.hybris.platform.cronjob.jalo.Job$JobRunable.run(Job.java:686)
	at de.hybris.platform.util.threadpool.PoolableThread.internalRun(PoolableThread.java:208)
	at de.hybris.platform.core.threadregistry.RegistrableThread.run(RegistrableThread.java:134)
Caused by: de.hybris.platform.jalo.flexiblesearch.FlexibleSearchException: could not translate value expression 'session.userSegments'[HY-0]
	at de.hybris.platform.jalo.flexiblesearch.SessionParamTranslator.translatePathValueKeys(SessionParamTranslator.java:91)
	at de.hybris.platform.jalo.flexiblesearch.FlexibleSearch.translatePathValueKeys(FlexibleSearch.java:2021)
	at de.hybris.platform.jalo.flexiblesearch.FlexibleSearch.search(FlexibleSearch.java:1426)
	at de.hybris.platform.jalo.flexiblesearch.FlexibleSearch.search(FlexibleSearch.java:1385)
	at de.hybris.platform.servicelayer.search.impl.DefaultFlexibleSearchService$2.execute(DefaultFlexibleSearchService.java:418)
	... 20 more

Flexible search system allows to execute custom code before processing query by implementing de.hybris.platform.servicelayer.search.preprocessor.QueryPreprocessor interface. Issues above can be resolved with QueryPreprocessor and population of custom session attributes in it. But it will lead to new unexpected exception:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Caused by: de.hybris.platform.jalo.flexiblesearch.FlexibleSearchException: could not translate value expression 'session.userSegments'
	at de.hybris.platform.jalo.flexiblesearch.SessionParamTranslator.translatePathValueKeys(SessionParamTranslator.java:91) ~[coreserver.jar:?]
	at de.hybris.platform.jalo.flexiblesearch.FlexibleSearch.translatePathValueKeys(FlexibleSearch.java:2021) ~[coreserver.jar:?]
	at de.hybris.platform.jalo.flexiblesearch.FlexibleSearch.search(FlexibleSearch.java:1426) ~[coreserver.jar:?]
	at de.hybris.platform.jalo.flexiblesearch.FlexibleSearch.search(FlexibleSearch.java:1380) ~[coreserver.jar:?]
	at de.hybris.platform.jalo.flexiblesearch.FlexibleSearch.search(FlexibleSearch.java:1343) ~[coreserver.jar:?]
	at de.hybris.platform.util.OneToManyHandler.getSearchResult(OneToManyHandler.java:163) ~[coreserver.jar:?]
	at de.hybris.platform.util.OneToManyHandler.getValues(OneToManyHandler.java:145) ~[coreserver.jar:?]
	at de.hybris.platform.catalog.jalo.GeneratedCatalogManager.getVariants(GeneratedCatalogManager.java:4920) ~[catalogserver.jar:?]
	at sun.reflect.GeneratedMethodAccessor1046.invoke(Unknown Source) ~[?:?]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_191]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_191]
	at de.hybris.platform.jalo.type.ReflectionAttributeAccess$AttributeMethod.invokeGetter(ReflectionAttributeAccess.java:751) ~[coreserver.jar:?]
	at de.hybris.platform.jalo.type.ReflectionAttributeAccess.getValue(ReflectionAttributeAccess.java:944) ~[coreserver.jar:?]
	at de.hybris.platform.jalo.Item.getAttribute(Item.java:1863) ~[coreserver.jar:?]
	at de.hybris.platform.servicelayer.internal.model.impl.JaloPersistenceObject.readRawValue(JaloPersistenceObject.java:111) ~[coreserver.jar:?]
	at de.hybris.platform.servicelayer.internal.converter.impl.ItemModelConverter.readSingleAttribute(ItemModelConverter.java:1381) ~[coreserver.jar:?]
	at de.hybris.platform.servicelayer.internal.converter.impl.ItemAttributeProvider.getAttribute(ItemAttributeProvider.java:108) ~[coreserver.jar:?]
	at de.hybris.platform.servicelayer.model.ItemModelContextImpl.loadUnlocalizedAttribute(ItemModelContextImpl.java:292) ~[coreserver.jar:?]
	at de.hybris.platform.servicelayer.model.ItemModelContextImpl.getValue(ItemModelContextImpl.java:252) ~[coreserver.jar:?]
	at de.hybris.platform.servicelayer.model.ItemModelContextImpl.getPropertyValue(ItemModelContextImpl.java:268) ~[coreserver.jar:?]
	at de.hybris.platform.core.model.product.ProductModel.getVariants(ProductModel.java:1373) ~[models.jar:?]
	at de.hybris.platform.commerceservices.search.solrfacetsearch.provider.AbstractMultidimensionalProductFieldValueProvider.isVariantBaseProduct(AbstractMultidimensionalProductFieldValueProvider.java:116) ~[classes/:?]
	at de.hybris.platform.commerceservices.search.solrfacetsearch.provider.impl.FirstVariantCategoryNameListValueProvider.getFieldValue(FirstVariantCategoryNameListValueProvider.java:45) ~[classes/:?]
	at de.hybris.platform.commerceservices.search.solrfacetsearch.provider.AbstractMultidimensionalProductFieldValueProvider.getFieldValues(AbstractMultidimensionalProductFieldValueProvider.java:49) ~[classes/:?]
	at de.hybris.platform.solrfacetsearch.indexer.impl.DefaultIndexer.addIndexedPropertyFieldsForOldApi(DefaultIndexer.java:508) ~[solrfacetsearchserver.jar:?]
	at de.hybris.platform.solrfacetsearch.indexer.impl.DefaultIndexer.addIndexedPropertyFields(DefaultIndexer.java:483) ~[solrfacetsearchserver.jar:?]
	at de.hybris.platform.solrfacetsearch.indexer.impl.DefaultIndexer.createInputDocument(DefaultIndexer.java:381) ~[solrfacetsearchserver.jar:?]
	at de.hybris.platform.solrfacetsearch.indexer.impl.DefaultIndexer.indexItems(DefaultIndexer.java:208) ~[solrfacetsearchserver.jar:?]
	... 11 more

As you can see exception is generated, while getting product variants with ProductModel.getVariants() method. Also you can see that hybris uses OneToManyHandler to execute flexible search queries for receiving related models:

1
2
3
    private SearchResult getSearchResult(SessionContext ctx, PK pk) {
        return FlexibleSearch.getInstance().search(ctx, this.query, Collections.singletonMap("key", pk), Item.class);
    }

Execution of flexible search queries from jalo layer(FlexibleSearch.getInstance()) skips QueryPreprocessor and exception could not translate value expression is generated again.

So the only way do fix all this exception is to add custom session attribute on session creation. Listener for AfterSessionCreationEvent can be created and used for population of session attribute, but such approach will not work in case of using async events in hybris. The only way to add custom attribute on session creation, which will definitely work with async events, is to create own implementation of JaloSession class and add custom attributes creation in init method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package com.blog.core.modifiedjalo;

import com.blog.core.session.SessionDefaultAttributeProvider;
import de.hybris.platform.commerceservices.jalo.CommerceJaloSession;
import de.hybris.platform.core.Registry;

import java.util.Map;

public class BlogJaloSession extends CommerceJaloSession {

    @Override
    protected void init() {
        Map<String, SessionDefaultAttributeProvider> attributeProviderMap =
                Registry.getCoreApplicationContext().getBeansOfType(SessionDefaultAttributeProvider.class);

        attributeProviderMap.values().forEach(attributeProvider -> attributeProvider.putAttributeInJaloSession(this));
    }
}

BlogJaloSession must extend CommerceJaloSession to be safely used on storefront. Registry.getCoreApplicationContext() is used to receive global hybris spring context, where beans of SessionDefaultAttributeProvider class could be found. Registry.getApplicationContext() will not suite here, because it returns current application lowest context, for backoffice application this method will return backoffice web application spring context, which doesn’t include beans defined in custom accelerator extensions (for example, blogcore, blogfacade etc).

SessionDefaultAttributeProvider interface implements putAttributeInJaloSession method, which puts empty string for custom attribute in current jalo session. Be aware that session.setAttribute method will remove attribute if attribute value equals null.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package com.blog.core.session;

import de.hybris.platform.jalo.JaloSession;
import de.hybris.platform.servicelayer.session.Session;

public interface SessionDefaultAttributeProvider {

    default void putAttributeInJaloSession(final JaloSession session) {
        session.setAttribute(getAttributeKey(), "");
    }

    String getAttributeKey();

    void putAttributeInSession(Session session);
}

To force hybris use custom implementation of JaloSession must be changed JaloSessionFactory:

1
2
3
4
5
    <!-- Add default session attributes from flexible search restrictions to flexible search jalo session-->
    <!--IMPORTANT: * Do not inject it somewhere. * Do not call getBean("jalosession") by yourself, always use JaloSession.getCurrentSession(). -->
    <bean id="jalosession" class="de.hybris.platform.jalo.JaloSessionFactory">
        <property name="targetClassName" value="com.blog.core.modifiedjalo.BlogJaloSession"/>
    </bean>

P.S. With implementation described above default solr index job will not find any products, due to flexible search restriction. To fix this problem you can extend User with additional attribute, which will skip restriction :

1
2
(?session.user.disableSegmentation)
OR EXISTS({{select {stp.pk} from {CxSegmentToProductRelation as stp JOIN CxSegment as s ON {stp.source} = {s.pk}} where {s.code} IN (?session.userSegments) AND {stp.target} = {item.pk}}})

P.P.S. Solr search queries should be adjusted to restrict product visibility on PLP. To implement flexible search restriction concept for solr queries it is enough to implement beforeSearch method of de.hybris.platform.solrfacetsearch.search.context.FacetSearchListener interface. Sample inplementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
package com.blog.core.segmentation.solr;

import com.blog.core.segmentation.SegmentationService;
import de.hybris.platform.core.model.user.CustomerModel;
import de.hybris.platform.core.model.user.UserModel;
import de.hybris.platform.servicelayer.user.UserService;
import de.hybris.platform.solrfacetsearch.config.IndexedProperty;
import de.hybris.platform.solrfacetsearch.search.QueryField;
import de.hybris.platform.solrfacetsearch.search.SearchQuery;
import de.hybris.platform.solrfacetsearch.search.context.FacetSearchContext;
import de.hybris.platform.solrfacetsearch.search.context.FacetSearchListener;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.BooleanUtils;

import javax.annotation.Resource;
import java.util.Map;
import java.util.Set;

public class SegmentationRestrictionFacetSearchListener implements FacetSearchListener {

    private static final String SEGMENTATION_SOLR_FIELD = "segments";
    private static final String NOT_EXISTING_SEGMENT_CODE = "DUMMY_SEGMENT_TO_GET_EMPTY_RESULTS_FOR_USER_WITHOUT_SEGMENT";

    @Resource
    private UserService userService;

    @Resource
    private SegmentationService segmentationService;

    @Override
    public void beforeSearch(FacetSearchContext facetSearchContext) {
        // If is not backoffice index and indexed type has segmentation attribute
        String indexType = facetSearchContext.getIndexedType().getIdentifier();
        if (!isBackofficeSearch(facetSearchContext) && hasSegmentationAttribute(facetSearchContext)) {
            UserModel currentUser = userService.getCurrentUser();
            if (currentUser instanceof CustomerModel && BooleanUtils.isNotTrue(currentUser.getDisableSegmentation())) {
                SearchQuery searchQuery = facetSearchContext.getSearchQuery();
                Set<String> segments = getCurrentUserSegmentCodes(currentUser);

                // Add filter by segment
                if (CollectionUtils.isNotEmpty(segments)) {
                    QueryField filterQuery = new QueryField(SEGMENTATION_SOLR_FIELD, SearchQuery.Operator.OR, SearchQuery.QueryOperator.EQUAL_TO, segments);
                    searchQuery.addFilterQuery(filterQuery);
                }
            }

        }
    }

    private Set<String> getCurrentUserSegmentCodes(UserModel user) {
        Set<String> segments = segmentationService.getUserSegmentCodes(user);

        // Solr syntax parser can't parse "segments_string_mv:". To filter out all products for user without segment
        // is added dummy segment code instead of empty string. Empty collection will also generate unparsable query.
        // ? Maybe switching on edismax syntax parser it would be possible to get rid of adding dummy segment code.
        if (segments.isEmpty()) {
            segments.add(NOT_EXISTING_SEGMENT_CODE);
        }

        return segments;
    }

    private boolean hasSegmentationAttribute(FacetSearchContext facetSearchContext) {
        Map<String, IndexedProperty> indexedProperties = facetSearchContext.getIndexedType().getIndexedProperties();
        IndexedProperty indexedProperty = indexedProperties.get(SEGMENTATION_SOLR_FIELD);
        return indexedProperty != null;
    }
    
    private boolean isBackofficeSearch(FacetSearchContext facetSearchContext) {
        if (isRegularCustomer()) {
            return isBackofficeIndexType(facetSearchContext);
        }
        return true;
    }

    private boolean isBackofficeIndexType(FacetSearchContext facetSearchContext) {
        final String indexType = facetSearchContext.getIndexedType().getIdentifier();
        return indexType.toLowerCase().contains("backoffice");
    }

    private boolean isRegularCustomer() {
        return userService.getCurrentUser() instanceof CustomerModel;
    }

    @Override
    public void afterSearch(FacetSearchContext facetSearchContext) {
        // No need to do anything after search. This Listener must add filter by segments before search execution.
    }

    @Override
    public void afterSearchError(FacetSearchContext facetSearchContext) {
        // No need to do anything after search. This Listener must add filter by segments before search execution.
    }
}
comments powered by Disqus