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