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