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.Locale;
021import java.util.Map;
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.StringUtils;
036
037import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
038import org.ametys.cms.repository.Content;
039import org.ametys.cms.search.GetQueryFromJSONHelper;
040import org.ametys.cms.search.QueryBuilder;
041import org.ametys.cms.search.SearchResults;
042import org.ametys.cms.search.Sort;
043import org.ametys.cms.search.content.ContentSearcherFactory;
044import org.ametys.cms.search.model.SearchCriterion;
045import org.ametys.cms.search.model.SearchModel;
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 query builder */
085    protected QueryBuilder _queryBuilder;
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        _queryBuilder = (QueryBuilder) smanager.lookup(QueryBuilder.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        // TODO Remove or replace by a custom search criterion AND contextual parameter.
126        // If true the contents marked as 'subContent' will be excluded
127        // boolean excludeSubContents = jsParameters.containsKey("excludeSubContents") ? (Boolean) jsParameters.get("excludeSubContents") : false;
128        
129        String originalWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
130        
131        try
132        {
133            if (StringUtils.isNotEmpty(workspaceName))
134            {
135                RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
136            }
137            
138            doSearch(request, model, begin, maxResults, jsParameters, contextualParameters);
139            
140            if (request.getAttribute(SEARCH_MODEL) == null)
141            {
142                request.setAttribute(SEARCH_MODEL, model);
143            }
144        }
145        catch (QuerySyntaxException e)
146        {
147            // Query syntax error: catch the error without logging or rethrowing it.
148            request.setAttribute(QUERY_ERROR, e.getI18nMessage());
149        }
150        catch (Exception e)
151        {
152            getLogger().error("Cannot search for contents : " + e.getMessage(), e);
153            throw new ProcessingException("Cannot search for contents : " + e.getMessage(), e);
154        }
155        finally
156        {
157            if (StringUtils.isNotEmpty(workspaceName))
158            {
159                RequestAttributeWorkspaceSelector.setForcedWorkspace(request, originalWorkspace);
160            }
161        }
162        
163        return EMPTY_MAP;
164    }
165    
166    /**
167     * Do the search and set the results in request attributes.
168     * @param request The request. The results or contents' id have to be set in request attributes
169     * @param model The search model
170     * @param offset The index of search
171     * @param maxResults The max results
172     * @param jsParameters The JS parameters
173     * @param contextualParameters The contextual parameters
174     * @throws Exception if the search failed 
175     */
176    @SuppressWarnings("unchecked")
177    protected void doSearch (Request request, SearchModel model, int offset, int maxResults, Map<String, Object> jsParameters, Map<String, Object> contextualParameters) throws Exception
178    {
179        try
180        {
181            Map<String, Object> values = (Map<String, Object>) jsParameters.get("values");
182            if (values == null)
183            {
184                values = Collections.emptyMap();
185            }
186            Map<String, List<String>> facetValues = (Map<String, List<String>>) jsParameters.get("facetValues");
187            if (facetValues == null)
188            {
189                facetValues = Collections.emptyMap();
190            }
191            String searchMode = StringUtils.defaultString((String) jsParameters.get("searchMode"), "simple");
192            
193            String sortInfo = (String) jsParameters.get("sort");
194            String groupInfo = (String) jsParameters.get("group");
195            List<Sort> sort = getSort(sortInfo, groupInfo);
196            
197            Map<String, ? extends SearchCriterion> criteria = searchMode.equals("advanced") && model instanceof SearchUIModel uiModel ? uiModel.getAdvancedCriteria(contextualParameters) : model.getCriteria(contextualParameters);
198            String lang = _queryBuilder.getCriteriaLanguage(criteria, searchMode, values, contextualParameters);
199            if (StringUtils.isNotEmpty(lang))
200            {
201                request.setAttribute(SEARCH_LOCALE, new Locale(lang));
202            }
203            
204            SearchResults<Content> results = _searcherFactory.create(model)
205                                                             .withSearchMode(searchMode)
206                                                             .withSort(sort)
207                                                             .withLimits(offset, maxResults)
208                                                             .searchWithFacets(values, facetValues, contextualParameters);
209            
210            
211            request.setAttribute(SEARCH_RESULTS, results);
212        }
213        catch (QuerySyntaxException e)
214        {
215            // Query syntax error: catch the error without logging or rethrowing it.
216            request.setAttribute(QUERY_ERROR, e.getI18nMessage());
217        }
218    }
219    
220    /**
221     * Get the search UI model
222     * @param jsParameters The JS parameters
223     * @param contextualParameters The contextual parameters
224     * @return the search UI model
225     */
226    protected SearchModel getSearchModel(Map<String, Object> jsParameters, Map<String, Object> contextualParameters)
227    {
228        return _getQueryFromJSONHelper.getSearchModel(jsParameters);
229    }
230    
231    /**
232     * Get the index of search
233     * @param jsParameters The JS parameters
234     * @return The offset
235     */
236    protected int getOffset(Map<String, Object> jsParameters)
237    {
238        return jsParameters.containsKey("start") ? (Integer) jsParameters.get("start") : 0; // Index of search
239    }
240    
241    /**
242     * Get the max number of results
243     * @param model The search model
244     * @param jsParameters The JS parameters
245     * @param contextualParameters The contextual parameters
246     * @return The max number of results
247     */
248    protected int getMaxResults(SearchModel model, Map<String, Object> jsParameters, Map<String, Object> contextualParameters)
249    {
250        int modelPageSize = model instanceof SearchUIModel uiModel ? uiModel.getPageSize(contextualParameters) : -1;
251        int maxResults = Integer.MAX_VALUE; // Number of results to generate
252        if (jsParameters.containsKey("limit"))
253        {
254            maxResults = (Integer) jsParameters.get("limit");
255        }
256        else if (modelPageSize >= 0)
257        {
258            maxResults = modelPageSize;
259        }
260        
261        if (maxResults < 0)
262        {
263            maxResults = Integer.MAX_VALUE;
264        }
265        
266        return maxResults;
267    }
268    
269    /**
270     * Get the sort criteria from a sort string.
271     * @param sortString The sort criteria as a JSON-encoded string.
272     * @param groupString The group criteria as a JSON-encoded string (for server-side grouping feature). Can be null.
273     * @return the sort criteria as a List of {@link Sort}.
274     */
275    protected List<Sort> getSort(String sortString, String groupString)
276    {
277        return _getQueryFromJSONHelper.getSort(sortString, groupString);
278    }
279}