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.PagesContainer;
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_SITE = "siteName";
059    private static final String __REQUEST_PARAM_LANG = "lang";
060    private static final String __REQUEST_PARAM_QUERY = "q";
061    private static final String __REQUEST_PARAM_SORT = "sort";
062
063    private SearcherFactory _searcherFactory;
064    
065    
066    @Override
067    public void service(ServiceManager serviceManager) throws ServiceException
068    {
069        super.service(serviceManager);
070        _searcherFactory = (SearcherFactory) serviceManager.lookup(SearcherFactory.ROLE);
071    }
072    
073    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
074    {
075        Request request = ObjectModelHelper.getRequest(objectModel);
076        
077        List<Map<String, String>> jsonResults = new ArrayList<>();
078        request.setAttribute(JSonReader.OBJECT_TO_READ, jsonResults);
079        
080        try
081        {
082            String siteName = _getSiteName(request); 
083            String lang = _getLang(parameters);
084            String query = _getQuery(request);
085            int limit = _getLimit(request);
086            Sort[] sorts = _getSorts(request);
087            
088            if (StringUtils.isNotBlank(query))
089            {
090                AmetysObjectIterable<Page> results = _queryPages(siteName, lang, query, limit, sorts);
091                
092                for (Page page : results)
093                {
094                    jsonResults.add(Map.of("title", page.getTitle(),
095                                           "long-title", page.getLongTitle(),
096                                           "id", page.getId(),
097                                           "url", ResolveURIComponent.resolve("page", page.getId()),
098                                           "path", String.join(" > ", _getParentsTitle(page))));
099                }
100            }
101            
102            
103        }
104        catch (Exception e)
105        {
106            throw new ProcessingException("Error while autocompleting", e);
107        }
108        
109        return EMPTY_MAP;
110    }
111
112    private String _getQuery(Request request)
113    {
114        return _escapeQuery(request.getParameter(__REQUEST_PARAM_QUERY));
115    }
116
117    private AmetysObjectIterable<Page> _queryPages(String siteName, String lang, String query, int limit, Sort[] sorts) throws Exception
118    {
119        OrQuery titleQuery = new OrQuery(List.of(new StringQuery(SolrWebFieldNames.PAGE_TITLE, Operator.SEARCH, query, lang, true),
120                                                 new StringQuery(SolrWebFieldNames.PAGE_LONG_TITLE, Operator.SEARCH, query, lang, true)));
121        
122        Searcher searcher = _searcherFactory.create()
123                .withQuery(titleQuery)
124                .addFilterQuery(new DocumentTypeQuery(SolrWebFieldNames.TYPE_PAGE))
125                .addFilterQueryString(SolrWebFieldNames.SITE_NAME + ":" + siteName)
126                .addFilterQueryString(SolrWebFieldNames.SITEMAP_NAME + ":" + lang)
127                .withLimits(0, limit)
128                .withSort(sorts)
129                .setCheckRights(true);
130
131        return searcher.search();
132    }
133    
134    private String _escapeQuery(String text)
135    {
136        if (text != null)
137        {
138            String trimText = StringUtils.strip(text.trim(), "*");
139            return "*" 
140                    + QueryHelper.escapeQueryCharsExceptStarsAndWhitespaces(trimText) 
141                    + "*";
142        }
143        return null;
144    }
145
146    private List<String> _getParentsTitle(Page page)
147    {
148        List<String> parents = new ArrayList<>();
149        
150        PagesContainer cursor = page.getParent();
151        while (cursor instanceof Page)
152        {
153            parents.add(0, ((Page) cursor).getTitle()); // insert at index 0, so the older parent is the first displayed
154            cursor = cursor.getParent();
155        }
156        
157        return parents;
158    }
159
160    private String _getLang(Parameters parameters)
161    {
162        String lang = parameters.getParameter(__REQUEST_PARAM_LANG, null);
163        if (StringUtils.isBlank(lang))
164        {
165            throw new IllegalArgumentException("A language must be specified for auto completion");
166        }
167        return lang;
168    }
169 
170    private String _getSiteName(Request request)
171    {
172        String siteName = WebHelper.getSiteName(request);
173        if (StringUtils.isBlank(siteName))
174        {
175            throw new IllegalArgumentException("A site must be specified for auto completion");
176        }
177        return siteName;
178    }
179    
180    private Sort[] _getSorts(Request request)
181    {
182        String sortParam = request.getParameter(__REQUEST_PARAM_SORT);
183        if (StringUtils.isNotBlank(sortParam) 
184            && ("true".equals(sortParam) || "title".equals(sortParam)))
185        {
186            return new Sort[] {new Sort("title_s_sort", Order.ASC)};
187        }
188        else
189        {
190            return new Sort[0];
191        }
192    }
193
194    private int _getLimit(Request request)
195    {
196        int limit = __DEFAULT_NUMBER;
197        String limitArg = request.getParameter("limit");
198        if (StringUtils.isNotBlank(limitArg))
199        {
200            limit = Integer.parseInt(limitArg);
201        }
202        limit = Math.min(__MAX_NUMBER, limit);
203        return limit;
204    }
205}