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}