001/* 002 * Copyright 2016 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.solr; 017 018import java.util.ArrayList; 019import java.util.Arrays; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.HashSet; 023import java.util.List; 024import java.util.Map; 025import java.util.Set; 026import java.util.stream.Collectors; 027 028import org.apache.avalon.framework.service.ServiceException; 029import org.apache.avalon.framework.service.ServiceManager; 030import org.apache.cocoon.environment.Request; 031import org.apache.commons.lang3.LocaleUtils; 032import org.apache.commons.lang3.StringUtils; 033 034import org.ametys.cms.contenttype.ContentTypesHelper; 035import org.ametys.cms.data.type.ModelItemTypeExtensionPoint; 036import org.ametys.cms.repository.Content; 037import org.ametys.cms.search.SearchResults; 038import org.ametys.cms.search.cocoon.SearchAction; 039import org.ametys.cms.search.content.ContentSearcherFactory.ContentSearchSort; 040import org.ametys.cms.search.content.ContentSearcherFactory.SearchModelContentSearcher; 041import org.ametys.cms.search.model.DefaultSearchModel; 042import org.ametys.cms.search.model.SearchModel; 043import org.ametys.cms.search.query.QuerySyntaxException; 044import org.ametys.cms.search.ui.model.ColumnHelper; 045import org.ametys.cms.search.ui.model.ColumnHelper.Column; 046import org.ametys.runtime.model.type.DataContext; 047import org.ametys.runtime.model.type.ElementType; 048import org.ametys.runtime.model.ViewItemContainer; 049 050/** 051 * Execute a solr query with custom columns and facets. 052 */ 053public class SolrQuerySearchAction extends SearchAction 054{ 055 /** The content types helper */ 056 protected ContentTypesHelper _contentTypesHelper; 057 058 /** The helper for columns */ 059 protected ColumnHelper _columnHelper; 060 061 /** The search type for parameters */ 062 protected ModelItemTypeExtensionPoint _solrModelItemTypeExtensionPoint; 063 064 @Override 065 public void service(ServiceManager serviceManager) throws ServiceException 066 { 067 super.service(serviceManager); 068 069 _contentTypesHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE); 070 _columnHelper = (ColumnHelper) serviceManager.lookup(ColumnHelper.ROLE); 071 _solrModelItemTypeExtensionPoint = (ModelItemTypeExtensionPoint) serviceManager.lookup(ModelItemTypeExtensionPoint.ROLE_SOLR_SEARCH); 072 } 073 074 @Override 075 protected void doSearch(Request request, SearchModel model, int offset, int maxResults, Map<String, Object> jsParameters, Map<String, Object> contextualParameters) throws Exception 076 { 077 SearchValues searchValues = getSearchValues(jsParameters); 078 Set<String> contentTypeIds = searchValues.getBaseContentTypes(); 079 080 DefaultSearchModel modelCopy = _searchModelHelper.copySearchModel(model, contextualParameters); 081 _searchModelHelper.addSolrFilterCriterion(modelCopy, getQueryString(searchValues), contextualParameters); 082 modelCopy.setContentTypes(contentTypeIds); 083 084 Collection<Column> columns = searchValues.getColumns(); 085 if (columns != null && !columns.isEmpty()) 086 { 087 ViewItemContainer resultItems = _columnHelper.createViewFromColumns(contentTypeIds, searchValues.getColumns(), false); 088 modelCopy.setResultItems(resultItems); 089 } 090 091 _searchModelHelper.setFacetedCriteria(modelCopy, searchValues.getFacets(), contextualParameters); 092 093 String lang = _searchModelHelper.getCriteriaLanguage(model, null, searchValues.getValues(), contextualParameters); 094 if (StringUtils.isNotEmpty(lang)) 095 { 096 request.setAttribute(SEARCH_LOCALE, LocaleUtils.toLocale(lang)); 097 } 098 099 List<ContentSearchSort> sorts = getSort(searchValues.getSortInfo(), searchValues.getGroupInfo()); 100 101 SearchResults<Content> results = getResults(searchValues, offset, maxResults, modelCopy, sorts, contextualParameters); 102 103 request.setAttribute(SEARCH_RESULTS, results); 104 request.setAttribute(SEARCH_MODEL, modelCopy); 105 } 106 107 /** 108 * Get the object representing search values from the JS parameters. 109 * @param jsParameters The JS parameters 110 * @return an object representing the search values 111 */ 112 protected SearchValues getSearchValues(Map<String, Object> jsParameters) 113 { 114 return new SearchValues(jsParameters); 115 } 116 117 /** 118 * Get the query string from the search values. 119 * @param searchValues The search values 120 * @return the query string 121 * @throws QuerySyntaxException if an error occurs 122 */ 123 @SuppressWarnings("unchecked") 124 protected String getQueryString(SearchValues searchValues) throws QuerySyntaxException 125 { 126 String baseQuery = searchValues.getBaseQuery(); 127 128 Map<String, Object> parameters = searchValues.getParameters(); 129 if (parameters != null && parameters.size() > 0) 130 { 131 for (Map.Entry<String, Object> entry : parameters.entrySet()) 132 { 133 Map<String, Object> scriptValue = (Map<String, Object>) entry.getValue(); 134 String typeId = (String) scriptValue.get("type"); 135 ElementType<Object> type = (ElementType) _solrModelItemTypeExtensionPoint.getExtension(typeId); 136 if (type == null) 137 { 138 throw new IllegalArgumentException("The solr search cannot handle type '" + typeId + "' for parameters."); 139 } 140 Object value = type.fromJSONForClient(scriptValue.get("value"), DataContext.newInstance()); 141 142 String token = type.toString(value); 143 144 baseQuery = baseQuery.replace("${" + entry.getKey() + "}", token); 145 } 146 } 147 148 return SolrContentQueryHelper.buildQuery(_searchModelHelper, baseQuery, Collections.EMPTY_SET/*content types or mixin query will be handled by the SimpleContentSearcher*/, searchValues.getWorkflowSteps()); 149 } 150 151 /** 152 * Create the searcher and execute it from the search values. 153 * @param searchValues The search values 154 * @param offset The offset 155 * @param maxResults The max number of results 156 * @param searchModel the search model 157 * @param sorts The sorts 158 * @param contextualParameters The contextual parameters 159 * @return the search results 160 * @throws Exception if an error occurs 161 */ 162 protected SearchResults<Content> getResults(SearchValues searchValues, int offset, int maxResults, SearchModel searchModel, List<ContentSearchSort> sorts, Map<String, Object> contextualParameters) throws Exception 163 { 164 return getContentSearcher(searchValues, offset, maxResults, searchModel, sorts) 165 .searchWithFacets(searchValues.getValues(), searchValues.getFacetValues(), contextualParameters); 166 } 167 168 /** 169 * Get the content search from the search values 170 * @param searchValues The search values 171 * @param offset The offset 172 * @param maxResults The max number of results 173 * @param searchModel the search model 174 * @param sorts The sorts 175 * @return the content searcher 176 */ 177 protected SearchModelContentSearcher getContentSearcher(SearchValues searchValues, int offset, int maxResults, SearchModel searchModel, List<ContentSearchSort> sorts) 178 { 179 return _searcherFactory.create(searchModel) 180 .withSort(sorts) 181 .withLimits(offset, maxResults); 182 } 183 184 /** 185 * Object representing search values. 186 */ 187 @SuppressWarnings("unchecked") 188 protected class SearchValues 189 { 190 /** The JS parameters */ 191 protected Map<String, Object> _jsParameters; 192 /** The values from JS parameters */ 193 protected Map<String, Object> _values; 194 /** The base query */ 195 protected String _baseQuery; 196 /** The query parameters */ 197 protected Map<String, Object> _parameters; 198 /** The content types */ 199 protected Set<String> _contentTypeIds; 200 /** The base content types (common content types) */ 201 protected Set<String> _baseContentTypeIds; 202 /** The facet fields */ 203 protected Collection<String> _facets; 204 /** The facet values */ 205 protected Map<String, List<String>> _facetValues; 206 /** The columns */ 207 protected Collection<Column> _columns; 208 /** The sorts */ 209 protected String _sortInfo; 210 /** The groups */ 211 protected String _groupInfo; 212 /** The workflow steps */ 213 protected Set<Integer> _wfSteps; 214 215 /** 216 * Constructor to build the object from JS parameters. 217 * @param jsParameters The JS parameters 218 */ 219 protected SearchValues(Map<String, Object> jsParameters) 220 { 221 _jsParameters = jsParameters; 222 _parseValues(); 223 _parseContentTypes(); 224 _parseQuery(); 225 _parseParameters(); 226 _parseFacets(); 227 _parseFacetValues(); 228 _parseColumns(); 229 _parseSortInfo(); 230 _parseGroupInfo(); 231 _parseWorkflowSteps(); 232 } 233 234 private void _parseValues() 235 { 236 _values = (Map<String, Object>) _jsParameters.get("values"); 237 } 238 239 private void _parseContentTypes() 240 { 241 _contentTypeIds = SolrContentQueryHelper.getContentTypes(_jsParameters); 242 _baseContentTypeIds = _contentTypesHelper.getCommonAncestors(_contentTypeIds); 243 } 244 245 private void _parseQuery() 246 { 247 _baseQuery = (String) _values.get("query"); 248 } 249 250 private void _parseParameters() 251 { 252 _parameters = (Map<String, Object>) _values.get("parameters"); 253 } 254 255 private void _parseFacets() 256 { 257 String facetObj = StringUtils.defaultString((String) _values.get("facets")); 258 _facets = Arrays.asList(StringUtils.split(facetObj, ", ")).stream().map(s -> s.replaceAll("\\.", "/")).collect(Collectors.toList()); 259 } 260 261 private void _parseFacetValues() 262 { 263 _facetValues = (Map<String, List<String>>) _jsParameters.get("facetValues"); 264 if (_facetValues == null) 265 { 266 _facetValues = Collections.emptyMap(); 267 } 268 } 269 270 private void _parseColumns() 271 { 272 Object columnsObject = _values.get("columns"); 273 if (columnsObject == null) 274 { 275 // Empty list, but not immutable 276 _columns = new ArrayList(); 277 } 278 else if (columnsObject instanceof String) 279 { 280 _columns = _columnHelper.getColumns((String) columnsObject, _baseContentTypeIds); 281 } 282 else if (columnsObject instanceof List) 283 { 284 _columns = _columnHelper.getColumns((List) columnsObject, _baseContentTypeIds); 285 } 286 } 287 288 private void _parseSortInfo() 289 { 290 _sortInfo = (String) _jsParameters.get("sort"); 291 } 292 293 private void _parseGroupInfo() 294 { 295 _groupInfo = (String) _jsParameters.get("group"); 296 } 297 298 private void _parseWorkflowSteps() 299 { 300 Object wfStepsObj = _values.get("workflowSteps"); 301 _wfSteps = new HashSet<>(); 302 if (wfStepsObj != null && wfStepsObj instanceof List<?>) 303 { 304 for (String wfStepObj : (List<String>) wfStepsObj) 305 { 306 if (StringUtils.isNotEmpty(wfStepObj)) 307 { 308 _wfSteps.add(Integer.parseInt(wfStepObj)); 309 } 310 } 311 } 312 } 313 314 /** 315 * Get the base query. 316 * @return the base query 317 */ 318 protected String getBaseQuery() 319 { 320 return _baseQuery; 321 } 322 323 /** 324 * Get the query parameters 325 * @return The parameters. Can be null. 326 */ 327 protected Map<String, Object> getParameters() 328 { 329 return _parameters; 330 } 331 332 /** 333 * Get the columns. 334 * @return the columns 335 */ 336 protected Collection<Column> getColumns() 337 { 338 return _columns; 339 } 340 341 /** 342 * Get the base content types (extract from content types, it's the common ancestors). 343 * @return the base content types 344 */ 345 protected Set<String> getBaseContentTypes() 346 { 347 return _baseContentTypeIds; 348 } 349 350 /** 351 * Get the content types. 352 * @return the content types 353 */ 354 protected Set<String> getContentTypes() 355 { 356 return _contentTypeIds; 357 } 358 359 /** 360 * Get the workflow steps. 361 * @return the workflow steps 362 */ 363 protected Set<Integer> getWorkflowSteps() 364 { 365 return _wfSteps; 366 } 367 368 /** 369 * Get the sort info. 370 * @return the sort info 371 */ 372 protected String getSortInfo() 373 { 374 return _sortInfo; 375 } 376 377 /** 378 * Get the group info. 379 * @return the group info 380 */ 381 protected String getGroupInfo() 382 { 383 return _groupInfo; 384 } 385 386 /** 387 * Get the facet fields. 388 * @return the facet fields 389 */ 390 protected Collection<String> getFacets() 391 { 392 return _facets; 393 } 394 395 /** 396 * Get the facet values. 397 * @return the facet values 398 */ 399 protected Map<String, List<String>> getFacetValues() 400 { 401 return _facetValues; 402 } 403 404 /** 405 * Get the values from the JS parameters. 406 * @return the values 407 */ 408 protected Map<String, Object> getValues() 409 { 410 return _values; 411 } 412 } 413}