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.Optional; 027import java.util.Set; 028import java.util.stream.Collectors; 029 030import org.apache.avalon.framework.container.ContainerUtil; 031import org.apache.avalon.framework.service.ServiceException; 032import org.apache.avalon.framework.service.ServiceManager; 033import org.apache.cocoon.environment.Request; 034import org.apache.commons.lang3.StringUtils; 035 036import org.ametys.cms.contenttype.ContentTypesHelper; 037import org.ametys.cms.repository.Content; 038import org.ametys.cms.search.QueryBuilder; 039import org.ametys.cms.search.SearchResults; 040import org.ametys.cms.search.Sort; 041import org.ametys.cms.search.cocoon.SearchAction; 042import org.ametys.cms.search.query.AndQuery; 043import org.ametys.cms.search.query.Query; 044import org.ametys.cms.search.query.QuerySyntaxException; 045import org.ametys.cms.search.query.WorkflowStepQuery; 046import org.ametys.cms.search.ui.model.ColumnHelper; 047import org.ametys.cms.search.ui.model.ColumnHelper.Column; 048import org.ametys.cms.search.ui.model.SearchUICriterion; 049import org.ametys.cms.search.ui.model.SearchUIModel; 050import org.ametys.core.util.AvalonLoggerAdapter; 051 052import com.google.common.primitives.Ints; 053 054/** 055 * Execute a solr query with custom columns and facets. 056 */ 057public class SolrQuerySearchAction extends SearchAction 058{ 059 /** The content types helper. */ 060 protected ContentTypesHelper _contentTypesHelper; 061 062 /** The helper for columns */ 063 protected ColumnHelper _columnHelper; 064 065 /** The searcher */ 066// protected Searcher _searcher; 067 068// protected ContentSearcherFactory _contentSearcher; 069 070 @Override 071 public void service(ServiceManager serviceManager) throws ServiceException 072 { 073 super.service(serviceManager); 074 _contentTypesHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE); 075 _columnHelper = (ColumnHelper) manager.lookup(ColumnHelper.ROLE); 076// _searcher = (Searcher) serviceManager.lookup(Searcher.ROLE); 077// _contentSearcher = (ContentSearcherFactory) serviceManager.lookup(ContentSearcherFactory.ROLE); 078 } 079 080 @SuppressWarnings("unchecked") 081 @Override 082 protected void doSearch(Request request, SearchUIModel model, int offset, int maxResults, Map<String, Object> jsParameters, Map<String, Object> contextualParameters) throws Exception 083 { 084 Map<String, Object> values = (Map<String, Object>) jsParameters.get("values"); 085 086 String baseQuery = (String) values.get("query"); 087 088 String columnsStr = StringUtils.defaultString((String) values.get("columns")); 089 090 String facetObj = StringUtils.defaultString((String) values.get("facets")); 091 Collection<String> facets = Arrays.asList(StringUtils.split(facetObj, ", ")).stream().map(s -> s.replaceAll("\\.", "/")).collect(Collectors.toList()); 092 093 Map<String, List<String>> facetValues = (Map<String, List<String>>) jsParameters.get("facetValues"); 094 if (facetValues == null) 095 { 096 facetValues = Collections.emptyMap(); 097 } 098 099 String sortInfo = (String) jsParameters.get("sort"); 100 String groupInfo = (String) jsParameters.get("group"); 101 List<Sort> sort = getSort(sortInfo, groupInfo); 102 103 Set<String> contentTypes = getContentTypes(jsParameters); 104 String baseContentType = Optional.ofNullable(contentTypes) 105 .map(_contentTypesHelper::getCommonAncestor) 106 .orElse(null); 107 108 Object wfStepsObj = values.get("workflowSteps"); 109 Set<Integer> wfSteps = new HashSet<>(); 110 if (wfStepsObj != null && wfStepsObj instanceof List<?>) 111 { 112 for (String wfStepObj : (List<String>) wfStepsObj) 113 { 114 if (StringUtils.isNotEmpty(wfStepObj)) 115 { 116 wfSteps.add(Integer.parseInt(wfStepObj)); 117 } 118 } 119 } 120 121 CriteriaSearchUIModelWrapper modelWrapper = new CriteriaSearchUIModelWrapper(model, manager, _context, new AvalonLoggerAdapter(getLogger())); 122 ContainerUtil.service(modelWrapper, manager); 123 124 facets = modelWrapper.setFacetedCriteria(baseContentType, facets, contextualParameters); 125 Collection<Column> columns = _getColumns(columnsStr, baseContentType); 126 modelWrapper.setResultColumns(baseContentType, columns, contextualParameters); 127 128 String queryStr = buildQuery(_queryBuilder, baseQuery, contentTypes, wfSteps); 129 130 Map<String, SearchUICriterion> criteria = model.getCriteria(contextualParameters); 131 String lang = _queryBuilder.getCriteriaLanguage(criteria, null, values, contextualParameters); 132 if (StringUtils.isNotEmpty(lang)) 133 { 134 request.setAttribute(SEARCH_LOCALE, new Locale(lang)); 135 } 136 137 SearchResults<Content> results = _searcherFactory.create(contentTypes) 138 .withSort(sort) 139 .withFacets(facets) 140 .withLimits(offset, maxResults) 141 .searchWithFacets(queryStr, facetValues); 142// AmetysObjectIterable<Content> contents = _searcher.search(solrQuery, sort, begin, maxResults); 143// SearchResults results = new LocalSearchResults(contents); 144 145 request.setAttribute(SEARCH_RESULTS, results); 146 request.setAttribute(SEARCH_MODEL, modelWrapper); 147 } 148 149 private Collection<Column> _getColumns(String columnsStr, String baseContentType) 150 { 151 return _columnHelper.getColumns(columnsStr, Optional.ofNullable(baseContentType)); 152 } 153 154 /** 155 * Gets the content types from JS parameters when using 'search-ui.solr' model 156 * @param jsParameters The JS parameters 157 * @return The content types 158 */ 159 @SuppressWarnings("unchecked") 160 public static Set<String> getContentTypes(Map<String, Object> jsParameters) 161 { 162 Map<String, Object> values = (Map<String, Object>) jsParameters.get("values"); 163 Object cTypesObj = values.get("contentTypes"); 164 if (cTypesObj != null && cTypesObj instanceof List<?>) 165 { 166 return new HashSet<>((List<String>) cTypesObj); 167 } 168 return Collections.emptySet(); 169 } 170 171 /** 172 * Build a Solr query string from inputs coming from a SolrQuerySearch (query, content types, workflow steps) 173 * @param queryBuilder The {@link QueryBuilder} component 174 * @param baseQuery The main query 175 * @param contentTypesOrMixins The content types -or mixins- (can be empty) 176 * @param wfSteps The workflow steps (can be empty) 177 * @return The built query 178 * @throws QuerySyntaxException If a query syntax is invalid 179 */ 180 public static String buildQuery(QueryBuilder queryBuilder, String baseQuery, Set<String> contentTypesOrMixins, Set<Integer> wfSteps) throws QuerySyntaxException 181 { 182 // Base query 183 List<Query> queriesInFinalQuery = new ArrayList<>(); 184 queriesInFinalQuery.add(() -> baseQuery); 185 186 // Content types 187 if (!contentTypesOrMixins.isEmpty()) 188 { 189 Query cTypeQuery = queryBuilder.createContentTypeOrMixinQuery(null, contentTypesOrMixins, true); 190 queriesInFinalQuery.add(0, cTypeQuery); 191 } 192 193 // Workflow steps 194 if (!wfSteps.isEmpty()) 195 { 196 WorkflowStepQuery wfStepQuery = new WorkflowStepQuery(Ints.toArray(wfSteps)); 197 queriesInFinalQuery.add(0, wfStepQuery); 198 } 199 200 Query query = new AndQuery(queriesInFinalQuery); 201 return query.build(); 202 } 203}