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