001package org.ametys.web.frontoffice.search.fast;
002/*
003 *  Copyright 2022 Anyware Services
004 *
005 *  Licensed under the Apache License, Version 2.0 (the "License");
006 *  you may not use this file except in compliance with the License.
007 *  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 *  Unless required by applicable law or agreed to in writing, software
012 *  distributed under the License is distributed on an "AS IS" BASIS,
013 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 *  See the License for the specific language governing permissions and
015 *  limitations under the License.
016 */
017
018import java.util.ArrayList;
019import java.util.List;
020import java.util.Map;
021
022import org.apache.avalon.framework.parameters.Parameters;
023import org.apache.avalon.framework.service.ServiceException;
024import org.apache.avalon.framework.service.ServiceManager;
025import org.apache.cocoon.ProcessingException;
026import org.apache.cocoon.acting.ServiceableAction;
027import org.apache.cocoon.environment.ObjectModelHelper;
028import org.apache.cocoon.environment.Redirector;
029import org.apache.cocoon.environment.Request;
030import org.apache.cocoon.environment.SourceResolver;
031import org.apache.commons.lang.StringUtils;
032
033import org.ametys.cms.search.Sort;
034import org.ametys.cms.search.Sort.Order;
035import org.ametys.cms.search.query.DocumentTypeQuery;
036import org.ametys.cms.search.query.OrQuery;
037import org.ametys.cms.search.query.Query.Operator;
038import org.ametys.cms.search.query.QueryHelper;
039import org.ametys.cms.search.query.StringQuery;
040import org.ametys.cms.search.solr.SearcherFactory;
041import org.ametys.cms.search.solr.SearcherFactory.Searcher;
042import org.ametys.cms.transformation.xslt.ResolveURIComponent;
043import org.ametys.core.cocoon.JSonReader;
044import org.ametys.plugins.repository.AmetysObjectIterable;
045import org.ametys.web.WebHelper;
046import org.ametys.web.indexing.solr.SolrWebFieldNames;
047import org.ametys.web.repository.page.Page;
048import org.ametys.web.repository.page.SitemapElement;
049
050/**
051 * Get propositions for autocompletion on title field while beginning a search on frontoffice
052 */
053public class FastSearchOnPageTitleAction extends ServiceableAction
054{
055    private static final int __MAX_NUMBER = 25;
056    private static final int __DEFAULT_NUMBER = 12;
057    
058    private static final String __REQUEST_PARAM_LANG = "lang";
059    private static final String __REQUEST_PARAM_QUERY = "q";
060    private static final String __REQUEST_PARAM_SORT = "sort";
061
062    private SearcherFactory _searcherFactory;
063    
064    
065    @Override
066    public void service(ServiceManager serviceManager) throws ServiceException
067    {
068        super.service(serviceManager);
069        _searcherFactory = (SearcherFactory) serviceManager.lookup(SearcherFactory.ROLE);
070    }
071    
072    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
073    {
074        Request request = ObjectModelHelper.getRequest(objectModel);
075        
076        List<Map<String, String>> jsonResults = new ArrayList<>();
077        request.setAttribute(JSonReader.OBJECT_TO_READ, jsonResults);
078        
079        try
080        {
081            String siteName = _getSiteName(request); 
082            String lang = _getLang(parameters);
083            String query = _getQuery(request);
084            int limit = _getLimit(request);
085            Sort[] sorts = _getSorts(request);
086            
087            if (StringUtils.isNotBlank(query))
088            {
089                AmetysObjectIterable<Page> results = _queryPages(siteName, lang, query, limit, sorts);
090                
091                for (Page page : results)
092                {
093                    jsonResults.add(Map.of("title", page.getTitle(),
094                                           "long-title", page.getLongTitle(),
095                                           "id", page.getId(),
096                                           "url", ResolveURIComponent.resolve("page", page.getId()),
097                                           "path", String.join(" > ", _getParentsTitle(page))));
098                }
099            }
100            
101            
102        }
103        catch (Exception e)
104        {
105            throw new ProcessingException("Error while autocompleting", e);
106        }
107        
108        return EMPTY_MAP;
109    }
110
111    private String _getQuery(Request request)
112    {
113        return _escapeQuery(request.getParameter(__REQUEST_PARAM_QUERY));
114    }
115
116    private AmetysObjectIterable<Page> _queryPages(String siteName, String lang, String query, int limit, Sort[] sorts) throws Exception
117    {
118        OrQuery titleQuery = new OrQuery(List.of(new StringQuery(SolrWebFieldNames.PAGE_TITLE, Operator.SEARCH, query, lang, true),
119                                                 new StringQuery(SolrWebFieldNames.PAGE_LONG_TITLE, Operator.SEARCH, query, lang, true)));
120        
121        Searcher searcher = _searcherFactory.create()
122                .withQuery(titleQuery)
123                .addFilterQuery(new DocumentTypeQuery(SolrWebFieldNames.TYPE_PAGE))
124                .addFilterQueryString(SolrWebFieldNames.SITE_NAME + ":" + siteName)
125                .addFilterQueryString(SolrWebFieldNames.SITEMAP_NAME + ":" + lang)
126                .withLimits(0, limit)
127                .withSort(sorts)
128                .setCheckRights(true);
129
130        return searcher.search();
131    }
132    
133    private String _escapeQuery(String text)
134    {
135        if (text != null)
136        {
137            String trimText = StringUtils.strip(text.trim(), "*");
138            return "*" 
139                    + QueryHelper.escapeQueryCharsExceptStarsAndWhitespaces(trimText) 
140                    + "*";
141        }
142        return null;
143    }
144
145    private List<String> _getParentsTitle(Page page)
146    {
147        List<String> parents = new ArrayList<>();
148        
149        SitemapElement cursor = page.getParent();
150        while (cursor instanceof Page)
151        {
152            parents.add(0, ((Page) cursor).getTitle()); // insert at index 0, so the older parent is the first displayed
153            cursor = cursor.getParent();
154        }
155        
156        return parents;
157    }
158
159    private String _getLang(Parameters parameters)
160    {
161        String lang = parameters.getParameter(__REQUEST_PARAM_LANG, null);
162        if (StringUtils.isBlank(lang))
163        {
164            throw new IllegalArgumentException("A language must be specified for auto completion");
165        }
166        return lang;
167    }
168 
169    private String _getSiteName(Request request)
170    {
171        String siteName = WebHelper.getSiteName(request);
172        if (StringUtils.isBlank(siteName))
173        {
174            throw new IllegalArgumentException("A site must be specified for auto completion");
175        }
176        return siteName;
177    }
178    
179    private Sort[] _getSorts(Request request)
180    {
181        String sortParam = request.getParameter(__REQUEST_PARAM_SORT);
182        if (StringUtils.isNotBlank(sortParam) 
183            && ("true".equals(sortParam) || "title".equals(sortParam)))
184        {
185            return new Sort[] {new Sort("title_s_sort", Order.ASC)};
186        }
187        else
188        {
189            return new Sort[0];
190        }
191    }
192
193    private int _getLimit(Request request)
194    {
195        int limit = __DEFAULT_NUMBER;
196        String limitArg = request.getParameter("limit");
197        if (StringUtils.isNotBlank(limitArg))
198        {
199            limit = Integer.parseInt(limitArg);
200        }
201        limit = Math.min(__MAX_NUMBER, limit);
202        return limit;
203    }
204}