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