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}