001/*
002 *  Copyright 2017 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.cocoon;
017
018import java.io.IOException;
019import java.util.ArrayList;
020import java.util.Collection;
021import java.util.List;
022import java.util.Locale;
023import java.util.Map;
024import java.util.Set;
025import java.util.stream.Collectors;
026
027import org.apache.avalon.framework.container.ContainerUtil;
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.cocoon.ProcessingException;
031import org.apache.cocoon.components.ContextHelper;
032import org.apache.cocoon.components.source.SourceUtil;
033import org.apache.cocoon.environment.Request;
034import org.apache.cocoon.xml.AttributesImpl;
035import org.apache.cocoon.xml.XMLUtils;
036import org.apache.commons.lang3.StringUtils;
037import org.apache.excalibur.source.Source;
038import org.xml.sax.SAXException;
039
040import org.ametys.cms.contenttype.ContentType;
041import org.ametys.cms.repository.Content;
042import org.ametys.cms.search.GroupSearchContentHelper;
043import org.ametys.cms.search.SearchResults;
044import org.ametys.cms.search.model.ResultField;
045import org.ametys.cms.search.model.SearchModel;
046import org.ametys.cms.search.solr.CriteriaSearchUIModelWrapper;
047import org.ametys.cms.search.ui.model.ColumnHelper.Column;
048import org.ametys.cms.search.ui.model.SearchUIColumn;
049import org.ametys.cms.search.ui.model.SearchUIModel;
050import org.ametys.core.util.AvalonLoggerAdapter;
051import org.ametys.core.util.IgnoreRootHandler;
052import org.ametys.plugins.repository.AmetysRepositoryException;
053
054/**
055 * Generate contents returned by the {@link SearchAction}, groups by selected fields.
056 */
057public class DocSearchGenerator extends SearchGenerator
058{
059    private GroupSearchContentHelper _groupSearchContentHelper;
060    
061    @Override
062    public void service(ServiceManager smanager) throws ServiceException
063    {
064        super.service(smanager);
065        _groupSearchContentHelper = (GroupSearchContentHelper) smanager.lookup(GroupSearchContentHelper.ROLE);
066    }
067    
068    @Override
069    protected void saxContents(SearchModel model, SearchResults<Content> results, Collection< ? extends ResultField> resultFields, String versionLabel, Map<String, Object> jsParameters, Locale locale, Map<String, Object> contextualParameters) throws SAXException, AmetysRepositoryException, IOException, ProcessingException
070    {
071        String modelId = (String) jsParameters.get("model");
072        SearchUIModel uiModel = _searchModelManager.getExtension(modelId);
073        
074        Set<String> cTypeIds = getCommonContentTypeIds(jsParameters, uiModel, contextualParameters);
075        
076        List<String> groupingFields = _getGroupingFields(jsParameters);
077
078        // Columns
079        List<Column> columns = getColumnsFromParameters(jsParameters, cTypeIds);
080
081        Collection< ? extends ResultField> resultFieldsFiltered = _filterResultFields(groupingFields, columns.stream().map(Column::getId).collect(Collectors.toList()), resultFields);
082        
083        CriteriaSearchUIModelWrapper modelWrapper = new CriteriaSearchUIModelWrapper(uiModel, manager, _context, new AvalonLoggerAdapter(getLogger()));
084        
085        try
086        {
087            ContainerUtil.service(modelWrapper, manager);
088            modelWrapper.setResultColumns(cTypeIds, columns, contextualParameters);
089        }
090        catch (Exception e)
091        {
092            throw new AmetysRepositoryException(e);
093        }
094        Map<String, SearchUIColumn> uiResultFields = modelWrapper.getResultFields(contextualParameters);
095        
096        Set<ContentType> contentTypes = cTypeIds.stream()
097                .map(_contentTypeExtensionPoint::getExtension)
098                .collect(Collectors.toSet());
099        GroupSearchContent groupSearchContent = _groupSearchContentHelper.organizeContentsInGroups(results.getObjects(), groupingFields, contentTypes, locale);
100        
101        // Set the request attributes from the contextual parameters
102        Request request = ContextHelper.getRequest(_context);
103        for (String name : contextualParameters.keySet())
104        {
105            request.setAttribute(name, contextualParameters.get(name));
106        }
107        
108        XMLUtils.startElement(contentHandler, "contents");
109        _saxGroup(groupSearchContent, 0, resultFieldsFiltered, uiResultFields, locale);
110        XMLUtils.endElement(contentHandler, "contents");
111    }
112    
113    private List<String> _getGroupingFields(Map<String, Object> jsParameters)
114    {
115        @SuppressWarnings("unchecked")
116        List<String> groupingFields = (List<String>) jsParameters.get("groupingFields");
117        if (groupingFields == null || groupingFields.size() == 1 && StringUtils.isEmpty(groupingFields.get(0)))
118        {
119            groupingFields = new ArrayList<>();
120        }
121        
122        return groupingFields;
123    }
124    
125    private Collection< ? extends ResultField> _filterResultFields(List<String> groupingFields, List<String> columns, Collection< ? extends ResultField> resultFields)
126    {
127        // Remove grouping fields from the result fields (they don't need to be displayed) and only keep columns requested
128        Collection<ResultField> resultFieldsFiltered = new ArrayList<>();
129        for (ResultField resultField : resultFields)
130        {
131            if (!groupingFields.contains(resultField.getId()) && (columns.size() == 0 || columns.contains(resultField.getId())))
132            {
133                resultFieldsFiltered.add(resultField);
134            }
135        }
136        
137        return resultFieldsFiltered;
138    }
139    
140    /**
141     * Sax a group of content recursively.
142     * First level (level=0) will not create a xml group (usually, 1st group is just here to handle a list)
143     * @param group group to sax
144     * @param level current level (start with zero)
145     * @param resultFields result fields to sax
146     * @param uiResultFields The columns to be exported
147     * @param locale The locale for search. Can be null.
148     * @throws SAXException if a error occurred during sax
149     * @throws AmetysRepositoryException if a error occurred
150     * @throws IOException if a error occurred
151     * @throws ProcessingException if a error occurred
152     */
153    private void _saxGroup(GroupSearchContent group, int level, Collection< ? extends ResultField> resultFields, Map<String, SearchUIColumn> uiResultFields, Locale locale) throws SAXException, AmetysRepositoryException, IOException, ProcessingException
154    {
155        if (level > 0)
156        {
157            AttributesImpl attrs = new AttributesImpl();
158            attrs.addCDATAAttribute("level", String.valueOf(level));
159            attrs.addCDATAAttribute("fullModelPath", group.getGroupFieldPath());
160            attrs.addCDATAAttribute("value", group.getGroupName());
161            XMLUtils.startElement(contentHandler, "group", attrs);
162        }
163        
164        for (GroupSearchContent groupSearchContent : group.getSubList())
165        {
166            _saxGroup(groupSearchContent, level + 1, resultFields, uiResultFields, locale);
167        }
168        
169        for (Content content : group.getContents())
170        {
171            _saxContentForDoc(content, resultFields, uiResultFields, locale);
172        }
173        
174        if (level > 0)
175        {
176            XMLUtils.endElement(contentHandler, "group");
177        }
178    }
179    /**
180     * SAX the result content
181     * @param content the result
182     * @param resultFields the result fields
183     * @param uiResultFields The columns to be exported
184     * @param locale The locale for search. Can be null.
185     * @throws SAXException if a error occurred during sax
186     * @throws AmetysRepositoryException if a error occurred
187     * @throws IOException if a error occurred
188     * @throws ProcessingException if a error occurred
189     */
190    private void _saxContentForDoc(Content content, Collection<? extends ResultField> resultFields, Map<String, SearchUIColumn> uiResultFields, Locale locale) throws SAXException, AmetysRepositoryException, IOException, ProcessingException
191    {
192        Request request = ContextHelper.getRequest(_context);
193        
194        try
195        {
196            request.setAttribute(Content.class.getName(), content);
197            request.setAttribute(ResultField.class.getName(), resultFields);
198            request.setAttribute("viewName", "export-word");
199            request.setAttribute("forceRemoteUrl", true);
200            
201            request.setAttribute("uiResultFields", uiResultFields);
202
203            AttributesImpl attrs = new AttributesImpl();
204            attrs.addCDATAAttribute("id", content.getId());
205            attrs.addCDATAAttribute("name", content.getName());
206            attrs.addCDATAAttribute("title", content.getTitle(locale));
207            if (content.getLanguage() != null)
208            {
209                attrs.addCDATAAttribute("language", content.getLanguage());
210            }
211                
212            XMLUtils.startElement(contentHandler, "content", attrs);
213            Source contentSource = resolver.resolveURI("cocoon:/export/content.doc" + (locale != null ? "?lang=" + locale.getLanguage() : ""));
214            SourceUtil.toSAX(contentSource, new IgnoreRootHandler(contentHandler));
215            
216            XMLUtils.endElement(contentHandler, "content");
217        }
218        finally
219        {
220            request.setAttribute(Content.class.getName(), null);
221            request.removeAttribute("forceRemoteUrl");
222        }
223    }
224    
225}