001/*
002 *  Copyright 2019 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.plugins.odfweb.service.search;
017
018import java.util.HashMap;
019import java.util.List;
020import java.util.Map;
021import java.util.stream.Collectors;
022
023import org.apache.avalon.framework.parameters.Parameters;
024import org.apache.avalon.framework.service.ServiceException;
025import org.apache.avalon.framework.service.ServiceManager;
026import org.apache.cocoon.ProcessingException;
027import org.apache.cocoon.acting.ServiceableAction;
028import org.apache.cocoon.environment.ObjectModelHelper;
029import org.apache.cocoon.environment.Redirector;
030import org.apache.cocoon.environment.Request;
031import org.apache.cocoon.environment.SourceResolver;
032import org.apache.commons.lang.StringUtils;
033import org.apache.solr.client.solrj.util.ClientUtils;
034
035import org.ametys.cms.content.indexing.solr.SolrFieldNames;
036import org.ametys.cms.repository.Content;
037import org.ametys.cms.search.query.ContentTypeQuery;
038import org.ametys.cms.search.query.DocumentTypeQuery;
039import org.ametys.cms.search.query.Query.Operator;
040import org.ametys.cms.search.query.StringQuery;
041import org.ametys.cms.search.solr.SearcherFactory;
042import org.ametys.cms.search.solr.SearcherFactory.Searcher;
043import org.ametys.cms.transformation.xslt.ResolveURIComponent;
044import org.ametys.core.cocoon.JSonReader;
045import org.ametys.odf.enumeration.OdfReferenceTableHelper;
046import org.ametys.odf.program.ProgramFactory;
047import org.ametys.plugins.repository.AmetysObjectIterable;
048import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector;
049import org.ametys.web.indexing.solr.SolrWebFieldNames;
050import org.ametys.web.repository.page.Page;
051import org.ametys.web.search.query.SiteQuery;
052import org.ametys.web.search.query.SitemapQuery;
053
054/**
055 * Get the proposed program's pages and skills for auto-completion while beginning a search
056 */
057public class AutocompletionSearchAction extends ServiceableAction
058{
059    /** The default max number of results */
060    protected static final int NB_MAX_RESULTS = 10;
061
062    /** The search factory */
063    protected 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        String siteName = request.getParameter("siteName");
078        if (StringUtils.isEmpty(siteName))
079        {
080            throw new IllegalArgumentException("A site must be specified for auto completion");
081        }
082        
083        String lang = request.getParameter("lang");
084        if (StringUtils.isEmpty(lang))
085        {
086            throw new IllegalArgumentException("A language must be specified for auto completion");
087        }
088        
089        int limit = request.getParameter("limit") != null ? Integer.valueOf(request.getParameter("limit")) : NB_MAX_RESULTS; 
090        
091        // Retrieve current workspace
092        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
093        
094        String contentType = parameters.getParameter("contentType", ProgramFactory.PROGRAM_CONTENT_TYPE);
095        
096        try
097        {
098            String query = request.getParameter("q");
099            String escapedQuery = _escapeQuery(query);
100            
101            Map<String, Object> results = new HashMap<>();
102            
103            // Search on page's title with the given content type
104            AmetysObjectIterable<Page> pageResults = getContentPages(contentType, siteName, lang, escapedQuery, limit);
105            List<Map<String, Object>> pages = pageResults.stream()
106                    .map(this::getPageHit)
107                    .collect(Collectors.toList());
108            results.put("pages", pages);
109            
110            // Search on skill's title
111            AmetysObjectIterable<Content> skillResults = getSkills(escapedQuery, lang, limit);
112            List<Map<String, Object>> skills = skillResults.stream()
113                    .map(this::getContentHit)
114                    .collect(Collectors.toList());
115            
116            results.put("skills", skills);
117            
118            request.setAttribute(JSonReader.OBJECT_TO_READ, results);
119            return EMPTY_MAP;
120        }
121        catch (Exception e)
122        {
123            getLogger().error("Error getting auto-complete list.", e);
124            throw new ProcessingException("Error getting auto-complete list.", e);
125        }
126        finally
127        {
128            // Restore context
129            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
130        }
131    }
132    
133    /**
134     * Get the content pages matching the query
135     * @param cType The content type of pages to search
136     * @param siteName the site name
137     * @param lang the language
138     * @param escapedQuery the query
139     * @param limit the max number of results
140     * @return the matching pages
141     * @throws Exception if an error occurred during search
142     */
143    protected AmetysObjectIterable<Page> getContentPages(String cType, String siteName, String lang, String escapedQuery, int limit) throws Exception
144    {
145        Searcher searcher = _searcherFactory.create()
146                .withQuery(new StringQuery(SolrWebFieldNames.PAGE_TITLE, Operator.LIKE, escapedQuery, null, true))
147                .addFilterQuery(new DocumentTypeQuery(SolrWebFieldNames.TYPE_PAGE))
148                .addFilterQuery(new SiteQuery(siteName))
149                .addFilterQuery(new SitemapQuery(lang))
150                .addFilterQueryString(SolrWebFieldNames.PAGE_CONTENT_TYPES + ":" + cType)
151                .withLimits(0, limit)
152                .setCheckRights(true);
153
154        return searcher.search();
155    }
156    
157    /**
158     * Get the skills contents matching the query
159     * @param escapedQuery the query
160     * @param lang the language
161     * @param limit the max number of results
162     * @return the matching contents
163     * @throws Exception if an error occurred during search
164     */
165    protected AmetysObjectIterable<Content> getSkills(String escapedQuery, String lang, int limit) throws Exception
166    {
167        Searcher searcher = _searcherFactory.create()
168                .withQuery(new StringQuery(SolrFieldNames.TITLE, Operator.LIKE, escapedQuery, lang, true))
169                .addFilterQuery(new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT))
170                .addFilterQuery(new ContentTypeQuery(OdfReferenceTableHelper.SKILL))
171                .withLimits(0, limit)
172                .setCheckRights(true);
173
174        return searcher.search();
175    }
176    
177    private String _escapeQuery(String text)
178    {
179        String trimText = StringUtils.strip(text.trim(), "*");
180        return "*" + ClientUtils.escapeQueryChars(trimText) + "*";
181    }
182    
183    /**
184     * Get the JSON representation of a page hit
185     * @param page the page
186     * @return the page as json
187     */
188    protected Map<String, Object> getPageHit(Page page)
189    {
190        Map<String, Object> result = new HashMap<>();
191        
192        result.put("title", page.getTitle());
193        result.put("id", page.getId());
194        result.put("url", ResolveURIComponent.resolve("page", page.getId()));
195        
196        return result;
197    }
198    
199    /**
200     * Get the JSON representation of a content hit
201     * @param content the content
202     * @return the content as json
203     */
204    protected Map<String, Object> getContentHit(Content content)
205    {
206        Map<String, Object> result = new HashMap<>();
207        
208        result.put("title", content.getTitle());
209        result.put("id", content.getId());
210        
211        return result;
212    }
213
214}