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}