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