001/*
002 *  Copyright 2015 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.cms.search.cocoon;
017
018import java.util.Collections;
019import java.util.List;
020import java.util.Map;
021import java.util.Objects;
022
023import org.apache.avalon.framework.context.Context;
024import org.apache.avalon.framework.context.ContextException;
025import org.apache.avalon.framework.context.Contextualizable;
026import org.apache.avalon.framework.parameters.Parameters;
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.cocoon.ProcessingException;
030import org.apache.cocoon.acting.ServiceableAction;
031import org.apache.cocoon.environment.ObjectModelHelper;
032import org.apache.cocoon.environment.Redirector;
033import org.apache.cocoon.environment.Request;
034import org.apache.cocoon.environment.SourceResolver;
035import org.apache.commons.lang3.LocaleUtils;
036import org.apache.commons.lang3.StringUtils;
037
038import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
039import org.ametys.cms.repository.Content;
040import org.ametys.cms.search.GetQueryFromJSONHelper;
041import org.ametys.cms.search.SearchResults;
042import org.ametys.cms.search.content.ContentSearcherFactory;
043import org.ametys.cms.search.content.ContentSearcherFactory.ContentSearchSort;
044import org.ametys.cms.search.model.SearchModel;
045import org.ametys.cms.search.model.SearchModelHelper;
046import org.ametys.cms.search.query.QuerySyntaxException;
047import org.ametys.cms.search.ui.model.SearchUIModel;
048import org.ametys.cms.search.ui.model.SearchUIModelExtensionPoint;
049import org.ametys.core.util.ServerCommHelper;
050import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector;
051
052/**
053 * Search contents and put a result object in the request (to be serialized in JSON).
054 */
055public class SearchAction extends ServiceableAction implements Contextualizable
056{
057    /** Name of request attribute for storing contents' ids */
058    public static final String SEARCH_CONTENTS = SearchAction.class.getName() + "$contentIds";
059    
060    /** Name of request attribute for storing search results */
061    public static final String SEARCH_RESULTS = SearchAction.class.getName() + "$searchResults";
062    
063    /** Name of request attribute for storing the language of search */
064    public static final String SEARCH_LOCALE = SearchAction.class.getName() + "$searchLocale";
065    
066    /** Name of request attribute for storing the search model */
067    public static final String SEARCH_MODEL = SearchAction.class.getName() + "$searchModel";
068    
069    /** Name of request attribute for storing the query error, if any. */
070    public static final String QUERY_ERROR = SearchAction.class.getName() + "$queryError";
071    
072    /** The search model manager */
073    protected SearchUIModelExtensionPoint _searchModelManager;
074    /** The ContentType Manager*/
075    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
076    /** The server comm helper */
077    protected ServerCommHelper _serverCommHelper;
078    /** The avalon context */
079    protected Context _context;
080    /** The searcher factory. */
081    protected ContentSearcherFactory _searcherFactory;
082    /** the helper to get query infos from JSON */
083    protected GetQueryFromJSONHelper _getQueryFromJSONHelper;
084    /** The search model helper */
085    protected SearchModelHelper _searchModelHelper;
086    
087    @Override
088    public void contextualize(Context context) throws ContextException
089    {
090        _context = context;
091    }
092    
093    @Override
094    public void service(ServiceManager smanager) throws ServiceException
095    {
096        super.service(smanager);
097        _searchModelManager = (SearchUIModelExtensionPoint) smanager.lookup(SearchUIModelExtensionPoint.ROLE);
098        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
099        _serverCommHelper = (ServerCommHelper) smanager.lookup(ServerCommHelper.ROLE);
100        _searcherFactory = (ContentSearcherFactory) smanager.lookup(ContentSearcherFactory.ROLE);
101        _getQueryFromJSONHelper = (GetQueryFromJSONHelper) smanager.lookup(GetQueryFromJSONHelper.ROLE);
102        _searchModelHelper = (SearchModelHelper) smanager.lookup(SearchModelHelper.ROLE);
103    }
104    
105    @SuppressWarnings("unchecked")
106    @Override
107    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
108    {
109        Request request = ObjectModelHelper.getRequest(objectModel);
110        
111        Map<String, Object> jsParameters = _serverCommHelper.getJsParameters();
112        
113        Map<String, Object> contextualParameters = (Map<String, Object>) jsParameters.get("contextualParameters");
114        if (contextualParameters == null)
115        {
116            contextualParameters = Collections.emptyMap();
117        }
118        
119        SearchModel model = getSearchModel(jsParameters, contextualParameters);
120        String workspaceName = parameters.getParameter("workspace", model.getWorkspace(contextualParameters));
121        
122        int begin = getOffset(jsParameters);
123        int maxResults = getMaxResults(model, jsParameters, contextualParameters);
124        
125        String originalWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
126        
127        try
128        {
129            if (StringUtils.isNotEmpty(workspaceName))
130            {
131                RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
132            }
133            
134            doSearch(request, model, begin, maxResults, jsParameters, contextualParameters);
135            
136            if (request.getAttribute(SEARCH_MODEL) == null)
137            {
138                request.setAttribute(SEARCH_MODEL, model);
139            }
140        }
141        catch (QuerySyntaxException e)
142        {
143            // Query syntax error: catch the error without logging or rethrowing it.
144            request.setAttribute(QUERY_ERROR, e.getI18nMessage());
145        }
146        catch (Exception e)
147        {
148            getLogger().error("Cannot search for contents : " + e.getMessage(), e);
149            throw new ProcessingException("Cannot search for contents : " + e.getMessage(), e);
150        }
151        finally
152        {
153            if (StringUtils.isNotEmpty(workspaceName))
154            {
155                RequestAttributeWorkspaceSelector.setForcedWorkspace(request, originalWorkspace);
156            }
157        }
158        
159        return EMPTY_MAP;
160    }
161    
162    /**
163     * Do the search and set the results in request attributes.
164     * @param request The request. The results or contents' id have to be set in request attributes
165     * @param model The search model
166     * @param offset The index of search
167     * @param maxResults The max results
168     * @param jsParameters The JS parameters
169     * @param contextualParameters The contextual parameters
170     * @throws Exception if the search failed
171     */
172    @SuppressWarnings("unchecked")
173    protected void doSearch (Request request, SearchModel model, int offset, int maxResults, Map<String, Object> jsParameters, Map<String, Object> contextualParameters) throws Exception
174    {
175        try
176        {
177            Map<String, Object> values = (Map<String, Object>) jsParameters.get("values");
178            if (values == null)
179            {
180                values = Collections.emptyMap();
181            }
182            Map<String, List<String>> facetValues = (Map<String, List<String>>) jsParameters.get("facetValues");
183            if (facetValues == null)
184            {
185                facetValues = Collections.emptyMap();
186            }
187            String searchMode = Objects.toString(jsParameters.get("searchMode"), "simple");
188            
189            String sortInfo = (String) jsParameters.get("sort");
190            String groupInfo = (String) jsParameters.get("group");
191            List<ContentSearchSort> sort = getSort(sortInfo, groupInfo);
192            
193            String lang = _searchModelHelper.getCriteriaLanguage(model, searchMode, values, contextualParameters);
194            if (StringUtils.isNotEmpty(lang))
195            {
196                request.setAttribute(SEARCH_LOCALE, LocaleUtils.toLocale(lang));
197            }
198            
199            SearchResults<Content> results = _searcherFactory.create(model)
200                                                             .withSearchMode(searchMode)
201                                                             .withSort(sort)
202                                                             .withLimits(offset, maxResults)
203                                                             .searchWithFacets(values, facetValues, contextualParameters);
204            
205            
206            request.setAttribute(SEARCH_RESULTS, results);
207        }
208        catch (QuerySyntaxException e)
209        {
210            // Query syntax error: catch the error without logging or rethrowing it.
211            request.setAttribute(QUERY_ERROR, e.getI18nMessage());
212        }
213    }
214    
215    /**
216     * Get the search UI model
217     * @param jsParameters The JS parameters
218     * @param contextualParameters The contextual parameters
219     * @return the search UI model
220     */
221    protected SearchModel getSearchModel(Map<String, Object> jsParameters, Map<String, Object> contextualParameters)
222    {
223        return _getQueryFromJSONHelper.getSearchModel(jsParameters);
224    }
225    
226    /**
227     * Get the index of search
228     * @param jsParameters The JS parameters
229     * @return The offset
230     */
231    protected int getOffset(Map<String, Object> jsParameters)
232    {
233        return jsParameters.containsKey("start") ? (Integer) jsParameters.get("start") : 0; // Index of search
234    }
235    
236    /**
237     * Get the max number of results
238     * @param model The search model
239     * @param jsParameters The JS parameters
240     * @param contextualParameters The contextual parameters
241     * @return The max number of results
242     */
243    protected int getMaxResults(SearchModel model, Map<String, Object> jsParameters, Map<String, Object> contextualParameters)
244    {
245        int modelPageSize = model instanceof SearchUIModel uiModel ? uiModel.getPageSize(contextualParameters) : -1;
246        int maxResults = Integer.MAX_VALUE; // Number of results to generate
247        if (jsParameters.containsKey("limit"))
248        {
249            maxResults = (Integer) jsParameters.get("limit");
250        }
251        else if (modelPageSize >= 0)
252        {
253            maxResults = modelPageSize;
254        }
255        
256        if (maxResults < 0)
257        {
258            maxResults = Integer.MAX_VALUE;
259        }
260        
261        return maxResults;
262    }
263    
264    /**
265     * Get the sort criteria from a sort string.
266     * @param sortString The sort criteria as a JSON-encoded string.
267     * @param groupString The group criteria as a JSON-encoded string (for server-side grouping feature). Can be null.
268     * @return the sort criteria as a List of {@link ContentSearchSort}.
269     */
270    protected List<ContentSearchSort> getSort(String sortString, String groupString)
271    {
272        return _getQueryFromJSONHelper.getSort(sortString, groupString);
273    }
274}