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