/******************************************************************************* * * Copyright 2019 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. * ******************************************************************************/ package com.adobe.cq.commerce.omnisearch; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.jcr.ItemExistsException; import javax.jcr.ItemNotFoundException; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.PathNotFoundException; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.UnsupportedRepositoryOperationException; import javax.jcr.Value; import javax.jcr.ValueFactory; import javax.jcr.lock.LockException; import javax.jcr.nodetype.ConstraintViolationException; import javax.jcr.observation.EventIterator; import javax.jcr.query.InvalidQueryException; import javax.jcr.query.QueryResult; import javax.jcr.query.Row; import javax.jcr.query.RowIterator; import javax.jcr.version.VersionException; import org.apache.jackrabbit.commons.iterator.RowIteratorAdapter; import org.apache.sling.api.resource.LoginException; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.api.resource.ResourceResolverFactory; import org.apache.sling.serviceusermapping.ServiceUserMapped; import org.osgi.service.component.ComponentContext; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Deactivate; import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.ReferenceCardinality; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.adobe.cq.commerce.api.conf.CommerceBasePathsService; import com.adobe.granite.omnisearch.commons.AbstractOmniSearchHandler; import com.adobe.granite.omnisearch.spi.core.OmniSearchHandler; import com.day.cq.commons.jcr.JcrConstants; import com.day.cq.search.QueryBuilder; import com.day.cq.search.result.SearchResult; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @Component(immediate = true, service = OmniSearchHandler.class) public class ProductsSuggestionOmniSearchHandler extends AbstractOmniSearchHandler { private static final Logger LOGGER = LoggerFactory.getLogger(ProductsSuggestionOmniSearchHandler.class); // We replace the legacy omnisearch component from Commerce Core private static final String TYPE = "product"; private static final String VIRTUAL_PRODUCT_QUERY_LANGUAGE = "virtualProductOmnisearchQuery"; private static final String PARAMETER_OFFSET = "_commerce_offset"; private static final String PARAMETER_LIMIT = "_commerce_limit"; @Reference(target = "(" + ServiceUserMapped.SUBSERVICENAME + "=omnisearch-service)") private ServiceUserMapped serviceUserMapped; @Reference private ResourceResolverFactory resolverFactory = null; @Reference private QueryBuilder queryBuilder = null; // For the search, we reuse the legacy component from Commerce Core @Reference( cardinality = ReferenceCardinality.MANDATORY, target = "(component.name=com.adobe.cq.commerce.impl.omnisearch.ProductsOmniSearchHandler)") private OmniSearchHandler productsOmniSearchHandler; // This is never used, it is declared to "enforce" the dependency to Commerce Core // so that this bundle is always loaded or restarted AFTER the Commerce Core bundle // so we properly override the 'product' omnisearch handler @Reference private CommerceBasePathsService cbps; @Activate protected void activate(ComponentContext componentContext) throws LoginException, PathNotFoundException, RepositoryException { if (resolver == null) { resolver = getResourceResolver(); init(resolver); jsonMapper = new ObjectMapper(); } } @Deactivate protected void deactivate(ComponentContext componentContext) throws LoginException { try { destroy(resolver); } finally { resolver.close(); } } @Override public String getID() { return TYPE; } private ResourceResolver resolver; private ObjectMapper jsonMapper; @Override public void onEvent(EventIterator eventIterator) { if (resolver == null) { try { resolver = getResourceResolver(); init(resolver); jsonMapper = new ObjectMapper(); } catch (LoginException e) { LOGGER.error("Error initializing!", e); } } } @Override public SearchResult getResults(ResourceResolver resolver, Map<String, Object> predicateParameters, long limit, long offset) { return productsOmniSearchHandler.getResults(resolver, predicateParameters, limit, offset); } protected javax.jcr.query.Query getSuperSuggestionQuery(ResourceResolver resolver, String searchTerm) { return super.getSuggestionQuery(resolver, searchTerm); } @Override public javax.jcr.query.Query getSuggestionQuery(ResourceResolver resolver, String searchTerm) { LOGGER.debug("Calling suggestion query with '{}'", searchTerm); return new SuggestionQueryWrapper(getSuperSuggestionQuery(resolver, searchTerm), searchTerm); } Iterator<Resource> getVirtualResults(ResourceResolver resolver, Map<String, Object> predicateParameters, long limit, long offset) { Map<String, Object> queryParameters = new HashMap<>(); queryParameters.putAll(predicateParameters); queryParameters.put(PARAMETER_OFFSET, String.valueOf(offset)); queryParameters.put(PARAMETER_LIMIT, String.valueOf(limit)); String queryString = mapToString(queryParameters); Iterator<Resource> virtualResults = null; try { virtualResults = resolver.findResources(queryString, VIRTUAL_PRODUCT_QUERY_LANGUAGE); } catch (Exception x) { LOGGER.error("Error searching virtual products", x); } return virtualResults; } String mapToString(Map<String, Object> map) { try { return jsonMapper.writeValueAsString(map); } catch (JsonProcessingException e) { LOGGER.error("Failed to serialize map data", e); return null; } } ResourceResolver getResourceResolver() throws LoginException { Map<String, Object> param = new HashMap<>(); param.put(ResourceResolverFactory.SUBSERVICE, OMNI_SEARCH_SERVICE_USER); return resolverFactory.getServiceResourceResolver(param); } private class SuggestionQueryWrapper implements javax.jcr.query.Query { private SuggestionQueryWrapper(javax.jcr.query.Query query, String searchTerm) { this.query = query; this.searchTerm = searchTerm; } private javax.jcr.query.Query query; private String searchTerm; @Override public QueryResult execute() throws InvalidQueryException, RepositoryException { // Get JCR results QueryResult queryResult = query.execute(); // Get CIF products Iterator<Resource> it = getVirtualResults(resolver, Collections.singletonMap("fulltext", searchTerm), 10, 0); if (it == null || !it.hasNext()) { return queryResult; // No CIF results } ValueFactory valueFactory = resolver.adaptTo(Session.class).getValueFactory(); List<Row> rows = new ArrayList<>(); while (it.hasNext()) { String title = it.next().getValueMap().get(JcrConstants.JCR_TITLE, String.class); Value value = valueFactory.createValue(title); rows.add(new SuggestionRow(value)); } RowIterator suggestionIterator = queryResult.getRows(); while (suggestionIterator.hasNext()) { rows.add(suggestionIterator.nextRow()); } SuggestionQueryResult result = new SuggestionQueryResult(rows); return result; } @Override public void setLimit(long limit) {} @Override public void setOffset(long offset) {} @Override public String getStatement() { return null; } @Override public String getLanguage() { return null; } @Override public String getStoredQueryPath() throws ItemNotFoundException, RepositoryException { return null; } @Override public Node storeAsNode(String absPath) throws ItemExistsException, PathNotFoundException, VersionException, ConstraintViolationException, LockException, UnsupportedRepositoryOperationException, RepositoryException { return null; } @Override public void bindValue(String varName, Value value) throws IllegalArgumentException, RepositoryException {} @Override public String[] getBindVariableNames() throws RepositoryException { return null; } } private class SuggestionQueryResult implements QueryResult { private List<Row> rows; private SuggestionQueryResult(List<Row> rows) { this.rows = rows; } @Override public String[] getColumnNames() throws RepositoryException { return null; } @Override public RowIterator getRows() throws RepositoryException { return new RowIteratorAdapter(rows.iterator()); } @Override public NodeIterator getNodes() throws RepositoryException { return null; } @Override public String[] getSelectorNames() throws RepositoryException { return null; } } private class SuggestionRow implements Row { private Value value; private SuggestionRow(Value value) { this.value = value; } @Override public Value[] getValues() throws RepositoryException { return null; } @Override public Value getValue(String columnName) throws ItemNotFoundException, RepositoryException { return value; } @Override public Node getNode() throws RepositoryException { return null; } @Override public Node getNode(String selectorName) throws RepositoryException { return null; } @Override public String getPath() throws RepositoryException { return null; } @Override public String getPath(String selectorName) throws RepositoryException { return null; } @Override public double getScore() throws RepositoryException { return 0; } @Override public double getScore(String selectorName) throws RepositoryException { return 0; } } }