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.Locale; 025import java.util.Map; 026import java.util.Set; 027import java.util.stream.Collectors; 028 029import org.apache.avalon.framework.container.ContainerUtil; 030import org.apache.avalon.framework.service.ServiceException; 031import org.apache.avalon.framework.service.ServiceManager; 032import org.apache.cocoon.environment.Request; 033import org.apache.commons.lang3.StringUtils; 034 035import org.ametys.cms.contenttype.ContentTypesHelper; 036import org.ametys.cms.repository.Content; 037import org.ametys.cms.search.SearchResults; 038import org.ametys.cms.search.Sort; 039import org.ametys.cms.search.cocoon.SearchAction; 040import org.ametys.cms.search.content.ContentSearcherFactory.SimpleContentSearcher; 041import org.ametys.cms.search.model.SearchCriterion; 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.core.util.AvalonLoggerAdapter; 047 048/** 049 * Execute a solr query with custom columns and facets. 050 */ 051public class SolrQuerySearchAction extends SearchAction 052{ 053 /** The content types helper */ 054 protected ContentTypesHelper _contentTypesHelper; 055 056 /** The helper for columns */ 057 protected ColumnHelper _columnHelper; 058 059 @Override 060 public void service(ServiceManager serviceManager) throws ServiceException 061 { 062 super.service(serviceManager); 063 064 _contentTypesHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE); 065 _columnHelper = (ColumnHelper) serviceManager.lookup(ColumnHelper.ROLE); 066 } 067 068 @Override 069 protected void doSearch(Request request, SearchModel model, int offset, int maxResults, Map<String, Object> jsParameters, Map<String, Object> contextualParameters) throws Exception 070 { 071 SearchValues searchValues = getSearchValues(jsParameters); 072 073 CriteriaSearchModelWrapper modelWrapper = new CriteriaSearchModelWrapper(model, manager, _context, new AvalonLoggerAdapter(getLogger())); 074 ContainerUtil.service(modelWrapper, manager); 075 076 modelWrapper.setResultColumns(searchValues.getBaseContentTypes(), searchValues.getColumns(), contextualParameters); 077 078 Map<String, ? extends SearchCriterion> criteria = model.getCriteria(contextualParameters); 079 String lang = _queryBuilder.getCriteriaLanguage(criteria, null, searchValues.getValues(), contextualParameters); 080 if (StringUtils.isNotEmpty(lang)) 081 { 082 request.setAttribute(SEARCH_LOCALE, new Locale(lang)); 083 } 084 085 Collection<String> transformedFacets = modelWrapper.setFacetedCriteria(searchValues.getBaseContentTypes(), searchValues.getFacets(), contextualParameters); 086 List<Sort> sorts = getSort(searchValues.getSortInfo(), searchValues.getGroupInfo()); 087 088 SearchResults<Content> results = getResults(searchValues, offset, maxResults, transformedFacets, sorts); 089 090 request.setAttribute(SEARCH_RESULTS, results); 091 request.setAttribute(SEARCH_MODEL, modelWrapper); 092 } 093 094 /** 095 * Get the object representing search values from the JS parameters. 096 * @param jsParameters The JS parameters 097 * @return an object representing the search values 098 */ 099 protected SearchValues getSearchValues(Map<String, Object> jsParameters) 100 { 101 return new SearchValues(jsParameters); 102 } 103 104 /** 105 * Get the query string from the search values. 106 * @param searchValues The search values 107 * @return the query string 108 * @throws QuerySyntaxException if an error occurs 109 */ 110 protected String getQueryString(SearchValues searchValues) throws QuerySyntaxException 111 { 112 return SolrContentQueryHelper.buildQuery(_queryBuilder, searchValues.getBaseQuery(), Collections.EMPTY_SET/*content types or mixin query will be handled by the SimpleContentSearcher*/, searchValues.getWorkflowSteps()); 113 } 114 115 /** 116 * Create the searcher and execute it from the search values. 117 * @param searchValues The search values 118 * @param offset The offset 119 * @param maxResults The max number of results 120 * @param facets Facets renamed for Solr 121 * @param sorts The sorts 122 * @return the search results 123 * @throws Exception if an error occurs 124 */ 125 protected SearchResults<Content> getResults(SearchValues searchValues, int offset, int maxResults, Collection<String> facets, List<Sort> sorts) throws Exception 126 { 127 return getContentSearcher(searchValues, offset, maxResults, facets, sorts) 128 .searchWithFacets(getQueryString(searchValues), searchValues.getFacetValues()); 129 } 130 131 /** 132 * Get the content search from the search values 133 * @param searchValues The search values 134 * @param offset The offset 135 * @param maxResults The max number of results 136 * @param facets Facets renamed for Solr 137 * @param sorts The sorts 138 * @return the content searcher 139 */ 140 protected SimpleContentSearcher getContentSearcher(SearchValues searchValues, int offset, int maxResults, Collection<String> facets, List<Sort> sorts) 141 { 142 return _searcherFactory.create(searchValues.getContentTypes()) 143 .withSort(sorts) 144 .withFacets(facets) 145 .withLimits(offset, maxResults); 146 } 147 148 /** 149 * Object representing search values. 150 */ 151 @SuppressWarnings("unchecked") 152 protected class SearchValues 153 { 154 /** The JS parameters */ 155 protected Map<String, Object> _jsParameters; 156 /** The values from JS parameters */ 157 protected Map<String, Object> _values; 158 /** The base query */ 159 protected String _baseQuery; 160 /** The content types */ 161 protected Set<String> _contentTypeIds; 162 /** The base content types (common content types) */ 163 protected Set<String> _baseContentTypeIds; 164 /** The facet fields */ 165 protected Collection<String> _facets; 166 /** The facet values */ 167 protected Map<String, List<String>> _facetValues; 168 /** The columns */ 169 protected Collection<Column> _columns; 170 /** The sorts */ 171 protected String _sortInfo; 172 /** The groups */ 173 protected String _groupInfo; 174 /** The workflow steps */ 175 protected Set<Integer> _wfSteps; 176 177 /** 178 * Constructor to build the object from JS parameters. 179 * @param jsParameters The JS parameters 180 */ 181 protected SearchValues(Map<String, Object> jsParameters) 182 { 183 _jsParameters = jsParameters; 184 _parseValues(); 185 _parseContentTypes(); 186 _parseQuery(); 187 _parseFacets(); 188 _parseFacetValues(); 189 _parseColumns(); 190 _parseSortInfo(); 191 _parseGroupInfo(); 192 _parseWorkflowSteps(); 193 } 194 195 private void _parseValues() 196 { 197 _values = (Map<String, Object>) _jsParameters.get("values"); 198 } 199 200 private void _parseContentTypes() 201 { 202 _contentTypeIds = SolrContentQueryHelper.getContentTypes(_jsParameters); 203 _baseContentTypeIds = _contentTypesHelper.getCommonAncestors(_contentTypeIds); 204 } 205 206 private void _parseQuery() 207 { 208 _baseQuery = (String) _values.get("query"); 209 } 210 211 private void _parseFacets() 212 { 213 String facetObj = StringUtils.defaultString((String) _values.get("facets")); 214 _facets = Arrays.asList(StringUtils.split(facetObj, ", ")).stream().map(s -> s.replaceAll("\\.", "/")).collect(Collectors.toList()); 215 } 216 217 private void _parseFacetValues() 218 { 219 _facetValues = (Map<String, List<String>>) _jsParameters.get("facetValues"); 220 if (_facetValues == null) 221 { 222 _facetValues = Collections.emptyMap(); 223 } 224 } 225 226 private void _parseColumns() 227 { 228 Object columnsObject = _values.get("columns"); 229 if (columnsObject == null) 230 { 231 // Empty list, but not immutable 232 _columns = new ArrayList(); 233 } 234 else if (columnsObject instanceof String) 235 { 236 _columns = _columnHelper.getColumns((String) columnsObject, _baseContentTypeIds); 237 } 238 else if (columnsObject instanceof List) 239 { 240 _columns = _columnHelper.getColumns((List) columnsObject, _baseContentTypeIds); 241 } 242 } 243 244 private void _parseSortInfo() 245 { 246 _sortInfo = (String) _jsParameters.get("sort"); 247 } 248 249 private void _parseGroupInfo() 250 { 251 _groupInfo = (String) _jsParameters.get("group"); 252 } 253 254 private void _parseWorkflowSteps() 255 { 256 Object wfStepsObj = _values.get("workflowSteps"); 257 _wfSteps = new HashSet<>(); 258 if (wfStepsObj != null && wfStepsObj instanceof List<?>) 259 { 260 for (String wfStepObj : (List<String>) wfStepsObj) 261 { 262 if (StringUtils.isNotEmpty(wfStepObj)) 263 { 264 _wfSteps.add(Integer.parseInt(wfStepObj)); 265 } 266 } 267 } 268 } 269 270 /** 271 * Get the base query. 272 * @return the base query 273 */ 274 protected String getBaseQuery() 275 { 276 return _baseQuery; 277 } 278 279 /** 280 * Get the columns. 281 * @return the columns 282 */ 283 protected Collection<Column> getColumns() 284 { 285 return _columns; 286 } 287 288 /** 289 * Get the base content types (extract from content types, it's the common ancestors). 290 * @return the base content types 291 */ 292 protected Set<String> getBaseContentTypes() 293 { 294 return _baseContentTypeIds; 295 } 296 297 /** 298 * Get the content types. 299 * @return the content types 300 */ 301 protected Set<String> getContentTypes() 302 { 303 return _contentTypeIds; 304 } 305 306 /** 307 * Get the workflow steps. 308 * @return the workflow steps 309 */ 310 protected Set<Integer> getWorkflowSteps() 311 { 312 return _wfSteps; 313 } 314 315 /** 316 * Get the sort info. 317 * @return the sort info 318 */ 319 protected String getSortInfo() 320 { 321 return _sortInfo; 322 } 323 324 /** 325 * Get the group info. 326 * @return the group info 327 */ 328 protected String getGroupInfo() 329 { 330 return _groupInfo; 331 } 332 333 /** 334 * Get the facet fields. 335 * @return the facet fields 336 */ 337 protected Collection<String> getFacets() 338 { 339 return _facets; 340 } 341 342 /** 343 * Get the facet values. 344 * @return the facet values 345 */ 346 protected Map<String, List<String>> getFacetValues() 347 { 348 return _facetValues; 349 } 350 351 /** 352 * Get the values from the JS parameters. 353 * @return the values 354 */ 355 protected Map<String, Object> getValues() 356 { 357 return _values; 358 } 359 } 360}