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;
028import java.util.stream.Stream;
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.SearchResults;
039import org.ametys.cms.search.Sort;
040import org.ametys.cms.search.cocoon.SearchAction;
041import org.ametys.cms.search.query.AndQuery;
042import org.ametys.cms.search.query.Query;
043import org.ametys.cms.search.query.WorkflowStepQuery;
044import org.ametys.cms.search.solr.CriteriaSearchUIModelWrapper.Column;
045import org.ametys.cms.search.ui.model.SearchUICriterion;
046import org.ametys.cms.search.ui.model.SearchUIModel;
047import org.ametys.core.util.AvalonLoggerAdapter;
048
049import com.google.common.primitives.Ints;
050
051/**
052 * Execute a solr query with custom columns and facets.
053 */
054public class SolrQuerySearchAction extends SearchAction
055{
056    /** The content types helper. */
057    protected ContentTypesHelper _contentTypesHelper;
058    
059    /** The searcher */
060//    protected Searcher _searcher;
061    
062//    protected ContentSearcherFactory _contentSearcher;
063    
064    @Override
065    public void service(ServiceManager serviceManager) throws ServiceException
066    {
067        super.service(serviceManager);
068        _contentTypesHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE);
069//        _searcher = (Searcher) serviceManager.lookup(Searcher.ROLE);
070//        _contentSearcher = (ContentSearcherFactory) serviceManager.lookup(ContentSearcherFactory.ROLE);
071    }
072    
073    @SuppressWarnings("unchecked")
074    @Override
075    protected void doSearch(Request request, SearchUIModel model, int offset, int maxResults, Map<String, Object> jsParameters, Map<String, Object> contextualParameters) throws Exception
076    {
077        Map<String, Object> values = (Map<String, Object>) jsParameters.get("values");
078        
079        List<Query> queriesInFinalQuery = new ArrayList<>();
080        queriesInFinalQuery.add(() -> (String) values.get("query"));
081        
082        String columnsStr = StringUtils.defaultString((String) values.get("columns"));
083        
084        String facetObj = StringUtils.defaultString((String) values.get("facets"));
085        Collection<String> facets = Arrays.asList(StringUtils.split(facetObj, ", ")).stream().map(s -> s.replaceAll("\\.", "/")).collect(Collectors.toList());
086        
087        Map<String, List<String>> facetValues = (Map<String, List<String>>) jsParameters.get("facetValues");
088        if (facetValues == null)
089        {
090            facetValues = Collections.emptyMap();
091        }
092        
093        String sortInfo = (String) jsParameters.get("sort");
094        String groupInfo = (String) jsParameters.get("group");
095        List<Sort> sort = getSort(sortInfo, groupInfo);
096        
097        Set<String> contentTypes = Collections.emptySet();
098        
099        Object cTypesObj = values.get("contentTypes");
100        String baseContentType = null;
101        if (cTypesObj != null && cTypesObj instanceof List<?>)
102        {
103            contentTypes = new HashSet<>((List<String>) cTypesObj);
104            baseContentType = _contentTypesHelper.getCommonAncestor(contentTypes);
105            
106            Query cTypeQuery = _queryBuilder.createContentTypeOrMixinQuery(null, contentTypes, true);
107            
108            queriesInFinalQuery.add(0, cTypeQuery);
109        }
110        
111        Object wfStepsObj = values.get("workflowSteps");
112        if (wfStepsObj != null && wfStepsObj instanceof List<?>)
113        {
114            Set<Integer> wfSteps = new HashSet<>();
115            for (String wfStepObj : (List<String>) wfStepsObj)
116            {
117                if (StringUtils.isNotEmpty(wfStepObj))
118                {
119                    wfSteps.add(Integer.parseInt(wfStepObj));
120                }
121            }
122            
123            if (!wfSteps.isEmpty())
124            {
125                WorkflowStepQuery wfStepQuery = new WorkflowStepQuery(Ints.toArray(wfSteps));
126                
127                queriesInFinalQuery.add(0, wfStepQuery);
128            }
129        }
130        
131        CriteriaSearchUIModelWrapper modelWrapper = new CriteriaSearchUIModelWrapper(model, manager, _context, new AvalonLoggerAdapter(getLogger()));
132        ContainerUtil.service(modelWrapper, manager);
133        
134        facets = modelWrapper.setFacetedCriteria(baseContentType, facets, contextualParameters);
135        modelWrapper.setResultColumns(baseContentType, getColumns(columnsStr), contextualParameters);
136        
137        Query query = new AndQuery(queriesInFinalQuery);
138        
139        SearchResults<Content> results = _searcherFactory.create(contentTypes)
140                .withSort(sort)
141                .withFacets(facets)
142                .withLimits(offset, maxResults)
143                .searchWithFacets(query, facetValues);
144//        AmetysObjectIterable<Content> contents = _searcher.search(solrQuery, sort, begin, maxResults);
145//        SearchResults results = new LocalSearchResults(contents);
146        
147        request.setAttribute(SEARCH_RESULTS, results);
148        request.setAttribute(SEARCH_MODEL, modelWrapper);
149        
150        Map<String, SearchUICriterion> criteria = model.getCriteria(contextualParameters);
151        String lang = _queryBuilder.getCriteriaLanguage(criteria, null, values, contextualParameters);
152        if (StringUtils.isNotEmpty(lang))
153        {
154            request.setAttribute(SEARCH_LOCALE, new Locale(lang));
155        }
156    }
157    
158    /**
159     * From a string representing columns, returns the list of column ids with their (optional) labels.
160     * @param columnsStr The columns as a string
161     * @return the list of column ids with their (optional) labels.
162     */
163    public static List<Column> getColumns(String columnsStr)
164    {
165        return _getColumns(Stream.of(StringUtils.split(columnsStr, ',')));
166    }
167    /**
168     * From a list of string representing columns, returns the list of column ids with their (optional) labels.
169     * @param columns The columns
170     * @return the list of column ids with their (optional) labels.
171     */
172    public static List<Column> getColumns(List<String> columns)
173    {
174        return _getColumns(columns.stream());
175    }
176    
177    private static List<Column> _getColumns(Stream<String> columns)
178    {
179        // in StringUtils.split, adjacent separators are treated as one separator, so col cannot be empty
180        // but it still can be whitespaces only, just ignore them silently
181        return columns.filter(StringUtils::isNotBlank)
182                      .map(SolrQuerySearchAction::_leftTrim) // because we do not want a column named " as " to be split then, the "as" should be considered as the column id
183                      .map(col -> col.split("(?i) AS ", 2))
184                      .map(arr ->
185                      {
186                          // col is never empty, so arr.length cannot be 0
187                          String colId = arr[0].trim().replace('.', '/');
188                          if (arr.length == 2)
189                          {
190                              return new Column(colId, arr[1].trim());
191                          }
192                          else
193                          {
194                              return new Column(colId, null);
195                          }
196                      })
197                      .distinct()
198                      .collect(Collectors.toList());
199    }
200    
201    private static String _leftTrim(String s)
202    {
203        return s.replaceAll("^\\s+", "");
204    }
205}